Java並發–三大性質
一、多執行緒的三大性質
原子性;可見性、有序性
二、原子性
原子性介紹
原子性是指:一個操作時不可能中斷的,要麼全部執行成功要麼全部執行失敗,有著同生共死的感覺。即使在多執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒所干擾。
先看看哪些是原子操作,哪些不是原子操作:
int a=10; //1
a++; //2
int b=a; //3
a=a+1; //4
上面這四個語句中只有第1個語句是原子操作,將10賦值給執行緒工作記憶體的變數a,而語句2 a++,實際上包含了三個操作:讀取變數a的值;對進行加1的操作,將計算後的值在賦值給變數a,而這三個操作都無法構成原子操作。對語句3,4的分析同理,這兩條語句不具備原子性。當然,Java記憶體模型中定義了8中操作都是原子的,不可再分的。
lock(鎖定):作用於主記憶體中的變數,它把一個變數標示為一個執行緒獨佔的狀態;
unlock(解鎖):作用於主記憶體中的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定;
read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體,以便後面的load動作使用;
load(載入):作用於工作記憶體中的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體中的變數副本;
use(使用):作用於工作記憶體中的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變數的值得位元組碼指令時將會執行這個操作;
assign(賦值):作用於工作記憶體中的變數,它把一個執行引擎接受到的值賦給工作記憶體的變數,每當虛擬機遇到一個給變數賦值的位元組碼指令時執行這個操作;
store(存儲):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給主記憶體中以便隨後的write操作使用;
write(操作):作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
上面的這些操作時相當底層的。那麼如何理解這些指令呢?比如:把一個變數從主記憶體複製到工作記憶體中就需要執行read,load操作,將工作記憶體同步到主記憶體中就需要執行store,write操作。注意的是:Java記憶體模型只是要求上述兩個操作時順序執行的並不是連續執行的。也就是說read和load之間可以插入其他指令,store和write也可以插入其他指令。比如對主記憶體中的a,b進行訪問就可以出現這樣的操作順序:read a,read b,load b,load a
由原子性操作變數read,load,use,assign,store,write可以大致認為基本數據類型的訪問讀寫具備原子性(例外的就是long和double的非原子性協定)
三、synchronized和volatile的原子性
synchronized
上面一共有八條原子操作,其中六條可以滿足基本數據類型的訪問讀寫具備原子性,還剩下lock和unlock兩條原子操作。
如果我們需要大範圍的原子操作就可以使用lock和unlock原子操作。儘管JVM沒有吧lock和unlock開放給我們,但JVM以更高層次的指令monitorenter和monitorexit開放給我們使用,反映到Java程式碼中就是—synchronized關鍵字,也就是說synchronized滿足原子性.
volatile
package passtra;
public class VolatileExample{
private static volatile int count=0;
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10000;i++){
count++;
}
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println(count);
}
}
開啟10個執行緒,每個執行緒都自加10000次,如果不出現執行緒安全的問題最終的結果應該是10*10000,可是運行多次都是小於100000。
問題在於volatile並不能保證原子性,count++並不是一個院子操作,包含了三個步驟:1、讀取變數count的值;2、對count加1;3、將新值賦給變數count。如果執行緒A讀取count到工作記憶體中,其他執行緒對這個值已經做了自增操作後,那麼執行緒A的值自然而然就是一個過期的值,因此,總結過必然會是小於100000的。
如果讓volatile保證原子性,就必須符合以下兩個原則:
運算結果並不依賴變數的當前的值,或者能夠確保只有一個執行緒修改變數的值;
變數不需要與其他的狀態變數共同參與不變約束。
四、synchronized和volatile的有序性
synchronized
synchronized語義表示鎖在同一時刻只能由一個執行緒進行獲取,當鎖被佔用後,其他執行緒只能等待。
因此,synchronized語義就要求執行緒在訪問讀寫操作共享變數時只能串列執行,因此synchronized具有有序性。
volatile
在JMM 中,為了性能優化,編譯器和處理器會進行指令重排序,也就是說Java程式天然的有序性,可以總結為:如果在本執行緒內觀察,所有的操作都是有序的,如果在一個執行緒觀察另一個執行緒,所有的操作都是無序的。
在單例模式的實現上有一種雙重檢驗鎖定的方式DCL(Double-checked Locking)
public class Singleton{ private static volatile Singleton singleton; private Singleton(){} public static Singleton getsingleton(){ if(singleton==null){ synchronized (Singleton.class) { if(singleton==null){ singleton=new Singleton(); } } } return singleton; } }
這裡為什麼要加volatile?先分析下不加volatile的情況,有問題的語句是這條:singleton=new Singleton();這條語句實際上包含了三個操作:1、分配對象的記憶體空間;2、初始化對象;3、設置singleton指向剛分配的記憶體地址。但由於存在重排序的問題,可能有以下的執行順序:如果2和3進行了重排序的話,執行緒B進行判斷if(singleton==null)時就會出現true,而實際撒花姑娘這個singleto並沒有初始化成功,顯而易見對B執行緒來說之後的操作就會是錯的。
而用volatile修飾的話就可以禁止2和3操重排序,從而避免這種情況
volatile包含禁止指令重排序的語義,其具有有序性。
五、synchronized和volatile的可見性
可見性是指:當一個執行緒修改了共享變數後,其他執行緒能夠立即得知這個修改。
synchronized:當執行緒獲取鎖時或從主記憶體中獲取共享變數的最新值,釋放鎖的時候會將共享變數同步到主記憶體中。所以,synchronized具有可見性
volatile:同樣在volatile分析中,會通過在指令中天機lock指令,一實現記憶體可見性,因此,volatile具有可見性
所以:synchronized具有原子性,有序性和可見性
volatile具有有序性和可見性