一個操做或者屢次操做,要麼全部的操做所有都獲得執行而且不會受到任何因素的干擾而中斷,要麼全部的操做都執行,要麼都不執行。html
對於基本數據類型的訪問,讀寫都是原子性的【long和double可能例外】。java
若是須要更大範圍的原子性保證,可使用synchronized關鍵字知足。git
當一個變量對共享變量進行了修改,另外的線程都能當即看到修改後的最新值。面試
volatile
保證共享變量可見性,除此以外,synchronized
和final
均可以 實現可見性。編程
synchronized
:對一個變量執行unclock以前,必須先把此變量同步回主內存中。緩存
final
:被final修飾的字段在構造器中一旦被初始化完成,而且構造器沒有把this的引用傳遞出去,其餘線程中就可以看見final字段的值。多線程
即程序執行的順序按照代碼的前後順序執行【因爲指令重排序的存在,Java 在編譯器以及運行期間對輸入代碼進行優化,代碼的執行順序未必就是編寫代碼時候的順序】,volatile
經過禁止指令重排序保證有序性,除此以外,synchronized
關鍵字也能夠保證有序性,由【一個變量在同一時刻只容許一條線程對其進行lock操做】這條規則得到。併發
計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程當中,勢必涉及到數據的讀取和寫入。因爲程序運行過程當中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,因爲CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,所以若是任什麼時候候對數據的操做都要經過和內存的交互來進行,會大大下降指令執行的速度。app
爲了解決CPU處理速度和內存不匹配的問題,CPU Cache出現了。ide
圖源:JavaGuide
當程序在運行過程當中,會將運算須要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束以後,再將高速緩存中的數據刷新到主存當中。
在單線程中運行是沒有任何問題的,可是在多線程環境下問題就會顯現。舉個簡單的例子,以下面這段代碼:
i = i + 1;
按照上面分析,主要分爲以下幾步:
多線程環境下,可能出現什麼現象呢?
最終的結果i = 1而不是i = 2,得出結論:若是一個變量在多個CPU中都存在緩存(通常在多線程編程時纔會出現),那麼就可能存在緩存不一致的問題。
解決緩存不一致的問題,一般來講有以下兩種解決方案【都是在硬件層面上提供的方式】:
經過在總線加LOCK#鎖的方式
在早期的CPU當中,是經過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。由於CPU和其餘部件進行通訊都是經過總線來進行的,若是對總線加LOCK#鎖的話,也就是說阻塞了其餘CPU對其餘部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。好比上面例子中 若是一個線程在執行 i = i +1,若是在執行這段代碼的過程當中,在總線上發出了LCOK#鎖的信號,那麼只有等待這段代碼徹底執行完畢以後,其餘CPU才能從變量i所在的內存讀取變量,而後進行相應的操做。這樣就解決了緩存不一致的問題。
但,有一個問題,在鎖住總線期間,其餘CPU沒法訪問內存,致使效率低下,因而就出現了下面的緩存一致性協議。
經過緩存一致性協議
較著名的就是Intel的MESI協議,MESI協議保S證了每一個緩存中使用的共享變量的副本是一致的。
當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的【嗅探機制:每一個處理器經過嗅探在總線上傳播的數據來檢查本身的緩存的值是否過時】,那麼它就會從內存從新讀取。
基於MESI一致性協議,每一個處理器須要不斷從主內存嗅探和CAS不斷循環,無效交互會致使總線帶寬達到峯值,出現總線風暴。
圖源:JavaFamily 敖丙三太子
JMM【Java Memory Model】
:Java內存模型,是java虛擬機規範中所定義的一種內存模型,Java內存模型是標準化的,屏蔽掉了底層不一樣計算機的區別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。
它描述了Java程序中各類變量【線程共享變量】的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節。
注意,爲了得到較好的執行性能,Java內存模型並無限制執行引擎使用處理器的寄存器或者高速緩存來提高指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java內存模型中,也會存在緩存一致性問題和指令重排序的問題。
全部的共享變量都存儲於主內存,這裏所說的變量指的是【實例變量和類變量】,不包含局部變量,由於局部變量是線程私有的,所以不存在競爭問題。
每一個線程都有本身的工做內存(相似於前面的高速緩存)。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。
每一個線程不能訪問其餘線程的工做內存。
在Java中,對基本數據類型的變量的讀取和賦值操做是原子性操做,即這些操做是不可被中斷的,要麼執行,要麼不執行。
爲了更好地理解上面這句話,能夠看看下面這四個例子:
x = 10; //1 y = x; //2 x ++; //3 x = x + 1; //4
須要注意的點:
Java提供了volatile關鍵字來保證可見性。
當一個共享變量被volatile修飾時,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,它會去內存中讀取新值。
另外,經過synchronized和Lock也可以保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖而後執行同步代碼,而且在釋放鎖以前會將對變量的修改刷新到主存當中。所以能夠保證可見性。
在Java內存模型中,容許編譯器和處理器對指令進行重排序,可是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
在Java裏面,能夠經過volatile關鍵字來保證有序性,另外也能夠經過synchronized和Lock來保證有序性。
Java內存模型具有一些先天的有序性,前提是兩個操做知足happens-before原則,摘自《深刻理解Java虛擬機》:
若是兩個操做的執行次序沒法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機能夠隨意地對它們進行重排序。
保證了不一樣線程對共享變量【類的成員變量,類的靜態成員變量】進行操做是時的可見性,一個線程修改了某個變量的值,新值對其餘線程來講是當即可見的。
禁止指令重排序。
舉個簡單的例子,看下面這段代碼:
//線程1 boolean volatile stop = false; while(!stop){ doSomething(); } //線程2 stop = true;
volatile沒法保證原子性,如對一個volatile修飾的變量進行自增操做i ++
,沒法保證多線程下結果的正確性。
解決方法:
下面這段話摘自《深刻理解Java虛擬機》:
觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令。
lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
- 它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;
- 它會強制將對緩存的修改操做當即寫入主存;
- 若是是寫操做,它會致使其餘CPU中對應的緩存行無效。
volatile變量讀操做的性能消耗與普通變量幾乎沒有什麼差異,可是寫操做則會慢一些,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。不過即使如此,大多數場景下volatile的總開銷仍然要比鎖來的低。
使用volatile必須具有兩個條件【保證原子】:
用雙重檢查鎖的方式實現單例模式:
public class Singleton { //注意使用volatile防止指令重排序 private volatile static Singleton instance; //私有化構造方法,單例模式基本操做 private Singleton() { } //靜態獲取單例的方法 public static Singleton getInstance() { //先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼 if (instance == null) { //類對象加鎖 synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
使用volatile
的緣由:防止指令重排序。
instance= new Singleton();
這一步,是一個實例化的過程,底層其實分爲三部執行:
- 爲instance分配內存空間:
memory = allocate();
- 實例化instance。
ctorInstance(memory);
- 將instance指向分配的內存地址。
instance = memory;
因爲JVM具備指令重排序的特性,指令的執行順序可能會變成1,3,2。在多線程環境下,可能某個線程可能會獲得未初始化的實例。
舉個例子:加入線程A執行了1和2以後,線程B調用getInstance的時候,會發現instance不爲null,會直接返回這個沒有執行過指令3的實例。