1.Java內存模型
1) Java虛擬機規範試圖定義一種Java內存模型,來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。
2)Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存中和從內存中取出變量這樣得底層細節。
3)Java內存模型規定了所有的變量都存儲在主內存中
4)每條線程都有自己的工作內存,線程的工作內存保存了該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存的變量。
2.內存間交互操作
Java內存模型定義了以下8種原子操作,來實現一個變量從主內存拷貝到工作內存以及從工作內存同步回主內存的實現細節。
lock(鎖定):作用於主內存的變量,它把一個變量標誌爲一條線程獨佔的狀態
unlock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放出來的變量纔可以被其他線程鎖定。
read:作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存,以便隨後的load操作。
load:作用於工作內存的變量,它把read操作的得到的變量放入工作內存的變量副本中。
use:作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要用到變量的值得字節碼指令時執行這個操作。
assign(賦值):作用於工作內存的變量,它把一個從執行引擎收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
store:作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後write操作使用。
write:作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。
3.原子性,可見性,有序性
原子性:由Java內存模型來直接保證的原子性變量操作包括read,load,assign,use,store和write,基本類型訪問讀寫是具備原子性的。通俗講,就是不可以再分爲多步操作了,它是最小操作單元。例如:a=2;具有原子性,但是i++;不具有原子性。它需要這樣三個步驟:1.取出i 2:計算i+1 3.計算結果寫入內存。
可見性:可見是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方式來實現可見性的。
有序性:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所以操作都是無序的。前半句是指:線程內表現爲串行,後邊句是指:指令重排序以及工作內存主內存同步延遲現象。
4.volatile:保證可見性,禁止指令重排序優化,但不保證原子性。
1)當一個變量定義爲volatile時,它保證了此變量對所有線程可見。當在讀取volatile變量時,會進行load操作(從主內存讀取,放入工作內存變量中)。當對volatile變量執行寫操作時,會在寫入後,進行store操作(把工作內存變量更新到主內存)。所以volatile具有可見性。
2)但是,volatile不保證原子性。
public class VolatileTest {
private volatile int count = 0;
public void incr() {
count++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
test.incr();
}
}
}).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(test.count);
}
}
發起5個線程,每個線程對inc進行100000次自增操作,若果能夠正確併發的話,應該輸出500000,但是輸出的是491783,並且每次運行,輸出結果都不一樣嗎,幾乎都低於50000.,因爲inc++不是原子操作。因此,volatile不能保證原子性。
在符合以下運算場景,才使用volatile變量控制併發:
(1) 運算結果並不依賴變量的當前值,或者能夠確保只有單一線程修改變量的值
(2) 變量不需要與其他的狀態變量共同參與不變約束
(3) volatile變量禁止指令重排序優化。
普通變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。
5.synchronized:保證原子性,可見性,有序性(指令重排)
基於Java內存模型, synchronized執行流程:
(1)線程獲得獲得互斥鎖
(2)清空工作內存
(3)在主內存中拷貝最新變量的副本到工作內存
(4)執行完代碼
(5)將更改後的共享變量的值刷新到內存
(6)釋放互斥鎖
synchronized一般有兩種使用方式,同步方法和同步代碼塊。Java虛擬機基於進入和退出Monitor對象來實現synchronized代碼塊同步和方法同步,但二者在字節碼層面的表現略有差別。
1. synchronized同步塊:JVM採用monitorenter、monitorexit兩個指令來實現同步。
2. synchronized方法:對於同步方法,JVM採用ACC_SYNCHRONIZED標記符來實現同步
即synchronized通過加鎖保證了原子性,可見性,有序性。
public class VolatileTest {
private int count = 0;
public synchronized void incr() {
count++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
test.incr();
}
}
}).start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(test.count);
}
}
發起5個線程,每個線程對inc進行100000次自增操作,能夠正確併發的話,每次都輸出500000。