最近想總結一些Java併發相關的內容,先寫吧,寫到哪兒就是哪[捂臉]java
在說明Java併發特性以前,先簡單瞭解一下物理計算機中的併發問題,這兩者有很多類似之處。物理機對併發的處理方案對於虛擬機也有很大的參考意義。緩存
「併發」在計算機領域內,一直是比較頭疼。由於併發不只僅是計算的事情,也是存儲的事情。咱們在處理併發時,不可能只靠CPU就能完成,也須要與內存交互,好比讀取運算數據,存儲運算結果等。多線程
可是,因爲CPU的處理效率和內存的處理效率差了幾個數量級,計算機不得不引入高速緩存做爲內存和CPU之間的緩衝,將運算須要使用的數據複製到緩存中,減小I/O瓶頸,加速運算,當運算完成以後,再將數據從緩存同步回內存中,這樣可以提高很多處理的效率。併發
不過,在引入高速緩存的同時,也帶來了另一個問題——緩存一致性。每一個處理器都有本身的高速緩存,而他們又共享同一主內存,當多個處理器任務都是涉及到同一塊主內存區域時,就會出現緩存數據不一致的問題。優化
同時,爲了解決一致性的問題,高速緩存就須要遵照一些一致性協議(MSI等協議)來規範對數據的讀寫。spa
具體示意圖,以下:操作系統
注:引入物理計算機併發的概念,主要是爲了提供一種思路,實際上的實現遠比描述的要複雜。線程
衆所周知,Java自己的運行是基於虛擬機的,在虛擬機的規範中,Java定義了一種內存模型,來屏蔽掉硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。3d
模型分爲主內存和工做內存,全部的變量(局部變量除外,局部變量都是線程私有的,不存在併發問題)都存儲在主內存中。每條線程具備本身的工做內存,其中工做內存中保存了線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接操做主內存中的變量。不一樣線程之間是沒法訪問對方的工做內存,線程間變量值的傳遞均須要經過主內存來完成,示意圖以下:code
注:這裏提到的主內存和工做內存,實際上和咱們常說的Java內存分爲堆、棧、方法區等並非同一層次的劃分,兩者基本上沒有直接聯繫。若是必定要勉強對應的話,那主內存主要對應於Java堆中的對象實例部分,而工做內存則對應於虛擬機棧中的部分區域。從更低層次上說,主內存直接對應於物理硬件的內存,而工做內存可能優先存儲於高速緩存中。
關於主內存與工做內存之間具體的交互協議,也就是說,一個變量如何從主內存拷貝到工做內存,又是如何從工做內存同步回到主內存的。Java定義了8種操做來實現的,而且虛擬機保證每一種操做都是原子的。
注:8種操做分別是lock、unlock、read、load、use、assign、store、write.
上圖所示,是兩組操做,一組是讀取,一組是寫入。
值得注意的是,Java模型只要求這兩個操做必須是順序執行,但並無保證是連續執行,這點區別是很是大的。
也就是說,read和load之間、store和write之間是能夠插入其餘指令的。
接下來,咱們關注一下,Java併發中的三個特性,原子性、可見性和有序性
由Java內存模型,咱們能夠得知,在工做內存和主內存交互時,儘管每一條指令是原子性的,可是每一組指令並非順序的。
好比,咱們對主內存中的變量a、b進行訪問時,一種可能出現的順序是
read a、read b、load b、load a.
所以,在多線程的環境下,會出現併發訪問主內存數據的問題。
那麼Java是如何知足原子性的需求呢?
Java內存模型中提供了lock和unlock操做來知足原子性的需求。
儘管虛擬機沒有直接給用戶提供操做,可是提供了更高層次的語法,這就是Java代碼中的同步塊——synchronized。
固然了,使用ReentrantLock也能夠知足原子性。
注:
基礎類型變量(byte、short、int、float、boolean等)都是原子性操做,不存在併發問題,經過反編譯成彙編語言,能夠看到基礎類型變量的操做只有一條彙編語句。
可是,對於long和dobule型,未必是原子性操做,主要緣由仍是由於這兩個都是8個字節(64位),Java規範容許將64位數據的讀寫操做劃分爲兩次32位的操做。
可見性指的是當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值。這種依賴主內存做爲傳遞媒介的方式來實現可見性的。
對於普通的變量來講,Java是不會保證可見性的,以下圖所示:
線程1和線程2都將x讀取到工做內存中,可是線程2將x的值改爲b,並無及時更新主內存,此時工做內存1仍然取值a。這就出現了可見性的問題。
Java是使用volatile關鍵字實現變量的可見性的。
volatile類型的變量,在修改值以後,會當即刷新到主內存。而且每次使用前都是從主內存中刷新。
注:synchronized同時也能實現可見性
爲了提高效率,儘可能充分利用計算能力,Java虛擬機的即時編譯器會對指令進行從新排序優化。
指令的從新排序,不保證程序中的各個語句執行的前後順序同代碼中的順序一致,可是它會保證程序最終結果執行結果和代碼順序執行的結果是一致的。
//同一個線程內
int a=10;//語句1
int r = 2;//語句2
a = a+3;//語句3
r= a*a;//語句4
複製代碼
執行的順序多是這樣的:
那有沒有可能語句4和語句3調換一下?
這種是不可能的,由於語句4依賴於語句3,因此語句4只能在語句3以後執行。
以上是單線程的狀況,在單線程中,儘管指令多是無序的,可是最終執行的結果是有序的。
那,換到多線程的狀況下,就不同了,咱們看一個例子。
//線程1
context = loadContext(); //語句1
inited = true; // 語句2
//線程2
while(!inited) {
sleep();
}
doSomething(context);
複製代碼
例子中,線程1的語句1和2沒有依賴性,所以可能會發生重排序。
假如發生了重排序,在線程1執行過程當中先執行了2,而此時線程2覺得初始化工做已經完成,那麼會跳出循環,執行doSomething(context)方法,而此時context並未初始化,會致使程序報錯。
這也就是無序會致使的併發問題。
Java提供了有序性保證的機制,經過volatile和synchronized均可以實現。
咱們將上面的代碼改造一下:
//線程1
context = loadContext(); //語句1
volatile inited = true; // 語句2
//線程2
while(!inited) {
sleep();
}
doSomething(context);
複製代碼
這樣的話,語句2和語句1的順序就會有保證了。
本文主要參考
《深刻理解Java虛擬機第二版》 周志明