以Java內存模型的角度看併發

以Java內存模型的角度看併發

Java的內存模型:

堆區:

  1. 存儲的全部是對象,每個對象都包含一個與之對應的class的信息。(class的目的是得到操作指令)。
  2. JVM的堆區(heap)被所有線程共享(相對於棧區,棧區的數據不共享),堆中不存放基本類型和對象引用,只存放對象本身。

棧區:

  1. 每個線程包含一個棧區,棧中只保存基礎數據類型的對象和自定義對象的引用(不是對象),對象都存放在堆區中。
  2. 每個棧中的數據(原始類型和對象引用)都是私有的,其他棧不能訪問。
  3. 棧分為3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令)。

方法區:

  1. 又叫靜態區,跟堆一樣,被所有的線程共享。方法區包含所有的class和static變量。
  2. 方法區中包含的都是在整個程序中永遠唯一的元素,如class,static變量。

方法調用棧:

在Java虛擬機進程中,每個線程都會擁有一個方法調用棧,用來跟蹤線程運行中一系列的方法調用過程,棧中的每一個元素就被稱為棧幀,每當線程調用一個方法的時候就會向方法棧壓入一個新幀。這裡的幀用來存儲方法的參數、局部變量和運算過程中的臨時數據。

每個線程都有自己的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區域。對象都在堆裡創建,為了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時 volatile 變量就可以發揮作用了,它要求線程從主存中讀取變量的值。

** Java中堆和棧有什麼不同?**

為什麼把這個問題歸類在多線程和併發面試題裡?因為棧是一塊和線程緊密相關的內存區域。每個線程都有自己的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區域。對象都在堆裡創建,為了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時volatile 變量就可以發揮作用了,它要求線程從主存中讀取變量的值。

線程封閉技術

如果在單線程內訪問數據,就不需要同步。這種技術被稱為線程封閉。

Java提供了一些機制來維持線程封閉性,例如局部變量和ThreadLocal類。

1)局部變量:

因為每個線程都有自己的方法調用棧,並且是私有的,所以訪問方法局部變量無需同步(其他線程併發執行同一個方法,得到的局部變量也不是同一個)

2)ThreadLocal類:

ThreadLocal是Java裡一種特殊的變量。每個線程都有一個ThreadLocal就是每個線程都擁有了自己獨立的一個變量,競爭條件被徹底消除了。它是為創建代價高昂的對象獲取線程安全的好方法,比如你可以用ThreadLocal讓SimpleDateFormat變成線程安全的,因為那個類創建代價高昂且每次調用都需要創建不同的實例所以不值得在局部範圍使用它,如果為每個線程提供一個自己獨有的變量拷貝,將大大提高效率。首先,通過複用減少了代價高昂的對象的創建個數。其次,你在沒有使用高代價的同步或者不變性的情況下獲得了線程安全。線程局部變量的另一個不錯的例子是ThreadLocalRandom類,它在多線程環境中減少了創建代價高昂的Random對象的個數。

上面是從其他文章&書本的整理知識介紹,下面來看從內存可見性的角度來看併發的問題:

1、DCL模式實現單例為什麼並不能保證線程安全

DCL模式實現單例,如下所示:

private static UserSingleton sInstance;
private UserSingleton () {

}
public static UserSingleton getInstance() {
if (sInstance == null) {
synchornized(UserSingleton.class){
if (sInstance == null) {
sInstance = new UserSingleton();

}
}
}
return sInstance;
}

因為在實例化對象這個地方new UserSingleton();這裡是實際上在JVM中進行三步指令操作(1、分配內存 2、執行構造函數並執行實例化變量 3、分配引用),而JVM對代碼編譯時會進行性能優化而對指令進行重排,因此123可能被優化成132,因此如果單例變量不是volatile修飾的,那麼可能在併發獲取單例的情況下,一個對象進入同步代碼塊,進行實例化,走到指令132步驟中的3時,此時指令2並未執行,但sInstance已經不為null了(已經分配了引用),另外一個線程查詢到sInstance已經不為null返回單例給客戶端,客戶端使用未初始化完成的單例進行操作,這時候會出現變量初始值不對引起的問題了。

以Java內存模型的角度看併發

解決辦法

單例變量使用volatile關鍵字修飾,即:

private static volatile UserSingleton sInstance;

volatile關鍵字可以保證變量的可見性,因為對volatile的操作都在Main Memory中,而Main Memory是被所有線程所共享的,這裡的代價就是犧牲了性能,無法利用寄存器或Cache,因為它們都不是全局的,無法保證可見性,可能產生髒讀。

volatile還有一個作用就是局部阻止重排序的發生,對volatile變量的操作指令都不會被重排序,因為如果重排序,又可能產生可見性問題。

在保證可見性方面,鎖(包括顯式鎖、對象鎖)以及對原子變量的讀寫都可以確保變量的可見性。但是實現方式略有不同,例如同步鎖保證得到鎖時從內存裡重新讀入數據刷新緩存,釋放鎖時將數據寫回內存以保數據可見,而volatile變量乾脆都是讀寫內存。

2、 競態條件為什麼不能通過volatile消除

volatile只保證可見性,不保證原子性,原子變量&加鎖機制可以保證可見性和原子性。

volatile與各類原子類區別:

volatile關鍵字只保證可見性,不保證原子性,它保證多個線程對volatile修飾的變量的寫操作會排在讀操作之前,但並不保證它修飾的變量操作具有原子性,例如volatile的count++依然不是原子操作,多個線程併發依然會出現競態條件,但AtomInteger可以使用getAndIncrement保證為原子操作,因此如果需要實現一個支持併發的計數器,不能使用以下代碼

private volatile int count;
count++;

而是要使用:

private AtomInteger mCount = new AtomInteger();
mCount.getAndIncrement();
以Java內存模型的角度看併發


分享到:


相關文章: