java 多線程 對象的安全發佈

簡單解釋下多線程中的重要概念


活躍性: 意味著正確的事情最終會發生

活躍性問題: 比如單線程中出現的無限循環

性能問題:和活躍性問題相對, 活躍性問題意味著正確的事情一定會發生,但是不夠好,我們希望正確的事情儘快發生

線程安全性: 一個類,被多個線程同時調用,且多個線程不需要額外的同步機制,就能保證這個類的方法被正確執行,那麼這個類就是安全的

原子性:count++就不是原子操作,它分為讀取修改寫入三個操作,boolean的讀寫是原子操作

競態條件: 最常見的就是先檢查後執行.意味著程序的正確性依賴於多線程執行時序的正確性

不變性條件:即一組操作,通過加鎖的方式,保證這組操作以原子操作的方式執行

複合操作:訪問一個狀態的所有操作,要以原子性的方式訪問, 即這組操作只能同時在同一個線程中執行,其他線程必須等待

重入: 同一個線程可以重入他獲得的所有鎖

重排序: 編譯器和運行時和處理器可能對操作進行重排序(處於性能方面的考慮), 比如構造器中初始化對象中的變量和返回這個對象引用,這兩個操作,可能出現構造器沒有完全初始化,引用就被返回,.這種現象被稱為不安全的發佈

可見性問題:當前線程對共享變量的修改對其他線程不可見,內置鎖可以保證同步代碼塊中的變量的更新對其他所有線程可見

發佈:將一個對象暴露到多線程環境中

逸出:一個本不該被暴露到多線程環境的對象被錯誤的暴露到多線程環境中

構造器中的this指針逸出:不論是逸出到多線程還是當前線程,都會出錯,可能讀取到的是未完全初始化的變量值,比如int i,首先默認設置為0,然後在構造器中初始化為100,如果構造器中this指針逸出,那this可能讀取到不正確的0值

安全地構造對象: 保證this指針不會再構造器中逸出即可,可以使用靜態方法進行初始化

線程封閉: 棧封閉:使用局部變量,局部變量一定是當前線程私有的, threadLocal 類:ThreadLocal中的數據被封閉在當前線程中

對象的不變性: 1,對象被正確創建(正確創建指的是構造過程this指針沒有逸出)

2,對象創建之後,狀態不允許被修改

3,所有的于都是final類型的(這個不需要,只要保證實際不可修改即可,比如吧對象放到ConccurencyMap中,外圍類只提供isContain()方法). 同時不可變對象一定是線程安全的

為什麼會產生安全發佈這個問題

出於性能方面的考慮. 編譯器,運行時,處理器可能會對操作進行重排序,比如創建對象時,執行構造器初始化實例的變量的操作,和返回該對象的引用這兩個操作,返回對象的引用可能先被執行,於是在多線程環境下,其他線程可能會看到一個部分構造的對象

[java]

view plain copy

public class Main { public static Person person; public static void main(String[] args) { person = new Person(100);//步驟2 } } class Person { int name = 0; public Person(int name) { this.name = name;//步驟1 } }

以上程序可能出現一種情況,步驟1沒有執行完畢,此時name的值依然是0,步驟2先執行了,同時person被髮布到多線程環境中,其他線程看到的不是一個完整的視圖,他看到了name=0,這種發佈被稱為不安全的發佈,為了保證我們看到的是一個完全初始化的對象,我們就需要進行安全的發佈

對象安全發佈的方式

在靜態初始化函數中初始化一個對象的引用

原理::虛擬機保證靜態初始化器初始化的對象一定能被安全的發佈

用volatile關鍵字標註對象的域或者使用AtomicReference來保存該對象

原理:volatile標註的域對象,一定能保證初始化完畢之後,對象引用才被返回

將對象保存在另一個對象的final域中,並在初始化構造器中初始化或者直接在域中初始化

原理:final標註的對象的域,一定能保證對象引用返回前,該對象中的final域已經成功初始化(除非出現this指針在構造器中逸出)

將對象保存在由鎖保存的域中

原理:鎖保證方法塊之內一個線程持有,使用這種方式要保證鎖返回前,內部所有的域都要被調用一次,(按筆者個人的理解,僅僅使用syncronized關鍵字並不足以保證對象被完整構造,可能導致對象的的引用被提前發佈)

線程安全的容器,能保證對象被安全發佈(比如SyncronizedMap,ConcurrencyMap,hashTable,等等)

接下來我們結合單例模式詳細分析安全發佈的問題

在靜態初始化函數中安全初始化一個對象的引用

首先我們定義Person類

[java]

view plain copy

class Person { private int name = 0; public Person(int name) { this.name = name; } public int getName() { return name; } }

以下是安全初始化Person類最簡單的方式:

[java] view plain copy

public class Singleton { private static Person person = new Person(100); public static Person getPerson() { return person; } }

如果初始化Person對象的工作量很大,我們可以使用內部類來延遲Person的初始化工作

[java]

view plain copy

public class Singleton { private static class PersonHolder{ public static Person person = new Person(10); } public static Person getPerson() { return PersonHolder.person; } }

這種方式保證了只有getPerson()方法被初次調用的時候PersonHolder類才會被初始化,

以上兩種方式都是筆者比較推崇的安全初始化的方式,

結合final或者volatile使用syncronized關鍵字安全地初始化一個對象

有些同學可能會用以下方式返回一個單例的看似安全的對象:

[java] view plain copy

class Singleton { private
static Person person; public static Person getPerson() { if (person==null) {// 操作1 synchronized (Person.class){ if (person==null) { person = new Person(100);//操作 2 } } } return person;// 操作3 } }

示例1

這段代碼看上去每次都能返回同一個對象,實際上這段代碼存在兩個問題:

一. 操作1和操作3是兩個普通讀, 可能會被重排序,導致操作3先去讀取,發現person為為null,然後另一個線程初始化了person對象,操作1讀取到person非空,最後返回的卻是null,這在單例模式中是不被允許的

二.同樣由於重排序,操作2發佈person的引用時,他的構造器可能還沒有構造完成,導致其他線程獲取到一個部分初始化的對象,這就是典型的不安全的發佈

為了解決問題一,我們可以引入局部變量instance,如下所示

[java] view plain copy

class Singleton { private static Person person; public static Person getPerson() { Person instance = person; if (instance == null) {//操作1 synchronized (Person.class) { instance=person; if (instance == null) { instance = new Person(100);//操作2 person = instance; } } } return person;//操作3 } }

由於局變量屬於線程私有的變量,線程私有的同一變量的讀-寫-讀不會被重排序,於是操作1,操作2,操作3不會被重排序

這就保證了getPerson()方法一定不會返回null值

接著我們解決問題二,處理安全發佈問題,這個是本文重點關注的問題,我們可以使用volatile或者final來保證對象的安全發佈

首先使用volatile關鍵字,代碼如下:

[java] view plain copy

class Singleton { private static volatile Person person; public static Person getPerson() { Person instance = person; if (instance == null) { synchronized (Person.class) { instance=person; if (instance == null) { instance = new Person(100); person = instance; } } } return person; } }

這裡我們只是加了一個volatile關鍵字,這就能保證person每次拿到對象引用的時候,該對象都已經初始化完畢.

接著我們使用final關鍵字來保證安全初始化:

[java] view plain copy

class
Singleton { private static volatile PersonWrapper personWrapper; public static Person getPersonWrapper() { PersonWrapper instance = personWrapper; if (instance == null) { synchronized (Person.class) { instance= personWrapper; if (instance == null) { instance = new PersonWrapper(new Person(100)); personWrapper = instance; } } } return personWrapper.person; } private static class PersonWrapper { public final Person person; private PersonWrapper(Person person) { this.person = person; } } }

雖然final也能保證初始化的安全性,但是代碼並不直觀

只使用synchronized關鍵字安全地初始化一個對象的引用

首先我們看看如下代碼:

[java] view plain copy

class Singleton { private static Person person; public static synchronized Person getPerson() { if (person == null) { person = new Person(100);//操作1 } return person; } }

[java] view plain copy

這段代碼同樣不能安全的發佈person對象,原因同示例1,那如果我們不想使用fianl或者volatile關鍵字,該怎麼辦呢,

我們可以在操作1之後,顯示讀取person對象中所有的域,這是因為線程的私有變量的寫和讀操作不能被重排序,這就能保證這些域在person引用發佈

時,都已經完全初始化了,具體代碼如下所示

[java] view plain copy

class Singleton22 { private static Person person; public static synchronized Person getPerson() { if (person == null) { if (person == null) { person = new Person(100); person.getName();// 顯示獲取所有對象域 } } return person; } }

安全發佈的內容我們就講完了

volatile和final關鍵字簡單比較

共同點:都能保證一個對象被安全的發佈,

不同點:

final.final的域,一旦初始化後不允許被修改,隨意每次都能讀到同一個值,(但是構造器中this指針逸出可能會出問題,不要這麼做)

final標註在類上,表示該類不允許被繼承,同時所有方法都被標註成final的,private的方法默認就是final的

final標註在方法上表示該方法不允許被重寫

volatile

volatile能保證long和double寫入操作是一個原子操作

volatile能保證內存可見性,volatile變量的寫操作多所有線程可見,但不能保證count++這種複合操作的原子性,其實count++其實是三個操作,讀-修改-寫入,注意:volatile 變量的寫操作不應該依賴於變量的原始值