併發程式設計的藝術-JAVA記憶體模型
併發程式設計的藝術-JAVA記憶體模型
1.JMM
1.1 什麼是JMM?
1.2 可見性分析與解決
1.2.1 CAS
1.2.2 ABA
1.3 什麼是原子性?
1.4 什麼是連續性?
2.Volatile
2.1 什麼是Volatile?
2.2 不保證原子性原因及分析
2.3 不保證原子性解決辦法
1.1 什麼是JMM?
Java記憶體模型(Java Memory Model簡稱JMM)是一種抽象的概念,並不真實存在,它描述的是一組規則或規範。
先看看JMM的特性:
可見性
原子性
有序性
1.2 可見性分析與解決
大家都知道,資料是存在主記憶體中的,執行緒想要操作主記憶體,需要先讀取主記憶體中的資料,然後操作後再寫入主記憶體,很簡單的流程,那麼為什麼需要可見性呢?
我們來看這樣一種情況,假如執行緒A和執行緒B同時操作主記憶體中的資料N,他們同時讀取了N的初始資料,然後執行緒A操作之後將修改後的資料寫入主記憶體,可是執行緒B不知道N已經被修改!!!這是很嚴重的錯誤,所以我們需要在主記憶體中的共享變數被修改後,通知其他正在操作這個資料的執行緒資料已修改,然後其他執行緒重新讀取主記憶體中的最新資料N,繼續操作,這個就是可見性,也是其存在的必要性。
可行性解決方案:
宣告共享變數為volatile
使用CAS的原子條件更新來實現執行緒之間的同步
配合以volatile的讀/寫和CAS所具有的volatile讀和寫的記憶體語義來實現執行緒之間的通訊
1.2.1 CAS
CAS,compare and swap,比較並交換。他要求執行緒在操作之後,要將資料寫入主記憶體之前,先判斷主記憶體中的資料是不是之前的資料,如果是則寫入主記憶體,如果不是則說明已經被其他執行緒修改過了,然後就會讀取最新資料然後繼續操作,這就是CAS。
CAS不是絕對完美的,他有自己的缺陷,比如每次要將資料寫入主記憶體時,主記憶體中的資料都被其他執行緒修改過了,他就會一直反覆迴旋。另外,CAS只能保證一個共享變數的原子操作。
最主要的是,CAS會出現ABA情況。
1.2.2 ABA
什麼是ABA?簡單理解就是,假如執行緒A要操作主記憶體中的某個資料,然後執行緒A操作時間假如10秒,但是在這10秒內,有另一個執行緒B操作了該資料,然後又將資料還原,等執行緒A將資料寫入主記憶體時,發現數據還是之前的資料,就會將資料寫入記憶體,可這個資料已經被執行緒B修改了兩次!!這就是ABA。
ABA解決辦法,上鎖,或者原子引用。
1.3 什麼是原子性?
原子性:一個操作不可分割的,不可分離的。舉個簡單例子,對於變數x,進行加1,然後取到值,這一個過程儘管簡單,但是卻不具備原子性,因為我們要先讀取x,之後進行計算,然後重新寫入,其實是幾個步驟。如果僅僅對x進行賦值,那麼則可以認為是原子的,JMM是保證其原子性的。
1.4 什麼是有序性?
我有一個朋友,他打字打累了,然後他決定隨便寫寫,他說,程式碼裡寫的程式碼,他不是按順序執行的,他會重新排序,然後執行,原因和細節想了解的請轉去原始碼。有序性就是要求他不重新排序。
2 Volatile
2.1 什麼是Volatile?
簡單來說,Volatile就是Java虛擬機器提供的一個輕量級的同步機制。
為什麼說是輕量級,大家先看看Volatitle的三個特點:
可見性
不保證其原子性
禁止指令重排
可見性我在JMM章節已經講了,禁止指令重排相當於JMM的連續性,在這裡也不多敘述了,我們重點分析下不保證其原子性這個特點。
2.2 不保證原子性原因及分析
大家都知道,JMM是保證其原子性的,即一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。在JMM篇章我們已經說過,為了保證可見性,我們需要用到Volatile去保證其可見性,那麼我們考慮下這個情況:
public class TestMain {
public static void main(String[] args) {
TestData testData=new TestData();//有個初始值為0的變數和一個加一的方法
//建立20個執行緒,每個執行緒執行1000次給變數加一的方法
for(int i=0;i<20;i++){
new Thread(()->{
for(int j=0;j<1000;j++){
testData.addOne();
}
},String.valueOf(i)).start();
}
//保證20個執行緒跑完
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"最後的值為:"+testData.number);
}
}
class TestData{
volatile int number=0;
public void addOne(){
number++;
}
}
假如我們建立20個執行緒,每個執行緒執行1000次給常量加一的方法,如果保證其原子性,正常的數字應該是20000,這點大家應該沒有疑惑,那麼最後結果是什麼呢?
無論你執行多少次,他最後的值永遠都不會是20000,那麼為什麼呢?
我們根據JMM的保證可見性的前提下思考一種情況,假如兩個執行緒同時操作主記憶體裡的同一個變數,初始值為0,然後執行緒A拿到變數後,給變數加1,在他想要將值寫入主記憶體的時候,執行緒B拿到了主記憶體中的變數,這個變數是初始值0,然後B執行緒拿到值之後,A執行緒將值寫入了主記憶體,次數主記憶體中的變數值為1,然後B執行緒執行完操作後值也為1,然後將值寫入主記憶體,同時寫了兩個1,主記憶體的值還是1,這就產生了丟失寫值的情況。所以無論我們執行多少次,他最後的值都不可能是20000。
因此,Volatile不保證其原子性。
2.2 不保證原子性解決辦法
解決辦法有兩種,一種是給方法上鎖,另一種就是原子引用(atomicreference)。CAS時原子引用會出現ABA問題,簡單理解就是假如A執行緒操作10秒,在這十秒內有另一個執行緒操作了資料,但又把資料還原,但是A執行緒發現不了,所以還是直接用時間戳原子引用(atomicstampedreference)吧。