主要參考:http://www.cnblogs.com/dolphin0520/
Java內存模型(Java Memory Model,JMM)
內存模型規定:
- 所有的變量都是存在主存當中(類似於物理內存)
- 每個線程都有自己的工作內存(類似於高速緩存)
- 線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。
- 每個線程不能訪問其他線程的工作內存。
緩存一致性(Cache coherence)問題
當程序在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中
在一個系統中,當許多不同的設備共享一個共同存儲器資源,在高速緩存中的數據不一致,就會產生問題。也就是說如果一個變量在多個CPU中都存在緩存(一般在多線程編程時纔會出現),那麼就可能存在緩存不一致的問題。
緩存一致性可以分爲三個層級:
- 在進行每個寫入運算時都立刻採取措施保證數據一致性
- 每個獨立的運算,假如它造成數據值的改變,所有進程都可以看到一致的改變結果
- 在每次運算之後,不同的進程可能會看到不同的值(這也就是沒有一致性的行爲)
爲了解決緩存不一致性問題,通常來說有以下2種解決方法:
1.通過在總線加LOCK鎖的方式
2.通過緩存一致性協議。
最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。
它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。
併發編程的三個問題:原子性問題,可見性問題,有序性問題
原子性
概念
一個操作或者多個操作,要麼全部執行,要麼就都不執行
執行的過程不會被任何因素打斷
只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)纔是原子操作。Java內存模型只保證了基本讀取和賦值是原子性操作。
Java語言提供的保證:
通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。
可見性
當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
普通的共享變量被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值。
Java語言提供的保證
volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
有序性
程序執行的順序按照代碼的先後順序執行。
指令重排序(Instruction Reorder)
處理器爲了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
處理器在進行重排序時是會考慮指令之間的數據依賴性。重排序會影響多個線程內程序執行的結果。換句話說,重排不會影響單個線程內程序執行的結果。
結論:指令重排的適用範圍是線程,它不會影響單個線程的執行,但是會影響到線程併發執行的正確性。
舉個參考資料裏的例子:
我相信,有經驗的程序員,即使不懂的指令重排,本能上也不會用兩個線程異步的執行下訴初始化操作。
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingWithConfig(context);
計劃執行:在線程1裏執行語句1,初始化加載操作,完成後,標誌位inited置爲true,線程2,跳出睡眠,執行doSomethingWithConfig方法。
實際執行:由於語句1和語句2沒有數據依賴性,因此可能會被重排序。假如發生了重排序,在線程1執行過程中先執行語句2,而此是線程2會以爲初始化工作已經完成,那麼就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context並沒有被初始化,就會導致程序出錯。
Java語言提供的保證:
通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性
volatile關鍵字
一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,那麼就具備了兩層語義:
1.保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2.禁止進行指令重排序。
當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。
volatile關鍵字的使用場景
volatile關鍵字是無法替代synchronized關鍵字的,因爲volatile關鍵字無法保證操作的原子性。因此,當保證操作是原子性操作,才能保證使用volatile關鍵字的程序在併發時能夠正確執行。
- 狀態標記量
- double check。
單例的最優寫法:(線程安全)
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
參考資料彙總