「天下武功,惟快不破」,火雲邪神告訴了你體術中追求的境界;相對論也告訴你們當你的移動速度逐漸超過光速甚至再快更快,你就很容易去到詩和遠方,遊火星,逛土星,浪跡天涯;當單核計算機從出現到一代代地提高性能,運算力也在更快更強。甚至就是奧運會都追求「更快、更高、更強」,彷佛「快」對人們有着與生俱來的誘惑。那麼「快節奏和從前慢一輩子只夠愛一我的」,你又有着怎樣的思考呢,抱歉~這裏暫不討論。其實啊,人們不斷壓榨計算工具的運算力和老闆不停壓榨員工的體力同樣也都是有快感的。既然是壓榨,總有一天可能幾近榨不出油水,咋辦?
這不,單核CPU的主頻不可能無限制的增加,Intel老闆給跪了。再想提高性能,因而CPU進入多核時代,多個處理器協同工做。什麼,協同工做?小學自習課最能咋呼的是你,中學最不服管的也是你,其實不少時候不是追求的一加一大於二,而是一加一不小於一,人越多越亂,事越多越煩,一個道理,增長CPU數量可不是簡單的一加一,變量越多,帶來的不肯定性也就越多,天賦異稟的人更適合作管理者,固然具有足夠完善和周密的算法的操做系統才能協同好計算機。程序員
多任務處理在現代計算機操做系統中幾乎是一項必備的功能。許多狀況下,讓計算機同時去作幾件事情,不只是由於計算機的運算能力強大了,還有一個很重要的緣由是計算機的運算速度與它的存儲和通訊子系統速度的差距太大。計算機的存儲設備與處理器的運算速度有幾個數量級的差距,因此現代計算機都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存來做爲內存與處理器之間的緩衝,這種解決思想呢就是緩衝技術。
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是也爲計算機系統帶來了更高的複雜度,引入了新的問題:緩存一致性。在多處理器系統中,每一個處理器都有本身的高速緩存,而又共享同一主內存。算法
有着相同目標,在不改變程序執行結果的前提下,儘量提升並行度。編程
處理器不會改變存在數據依賴關係的兩個操做的執行順序。緩存
處理器保證單線程程序的重排序不改變執行結果。多線程
越是追求性能的處理器,內存模型設計得越弱,束縛少,儘量多的優化來提升性能。併發
編譯器不會改變存在數據依賴關係的兩個操做的執行順序。app
除了增長高速緩存以外,爲了使得處理器內部的運算單元能儘可能被充分利用,處理器可能會對輸入代碼進行亂序執行優化,處理器會在計算以後將亂序執行的結構重組。ide
若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,那麼這兩個操做之間存在數據依賴性。
1)寫這個變量後,再讀這個變量;
2)寫這個變量後,再寫這個變量;
3)讀這個變量後,再寫這個變量;
上面3種狀況,只要重排兩個操做的執行順序,執行結果就會被改變。函數
語義是:無論編譯器和處理器爲了提升並行度怎麼重排序,單線程程序的執行結果不能被改變。工具
happens-before要求禁止的重排序分爲兩類:
1)會改變程序執行結果的重排序。
JMM處理策略要求編譯器和處理器必須禁止這種重排序。
2)不會改變程序執行結果的重排序。
JMM處理策略容許編譯器和處理器這種重排序。
規則定義:
1)程序順序規則:一個線程中的每一個操做,happens-before該線程的任意後續操做。
2)監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
3)volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
4)傳遞性:若是A happens-before B,且B happens-before C,那麼A happens-before C。
5)start()規則:若是線程A執行操做ThreadB.start()啓動線程B,那麼A線程的ThreadB.start()操做happens-before於線程B中的任意操做。
6)join()規則:若是線程A執行操做Thread.join()併成功返回,那麼線程B中的任意操做happens-before於線程A從Thread.join()操做成功返回。
1)as-if-serial語義保證單線程內程序的執行結果不被改變;happens-before關係保證正確同步的多線程程序的執行結果不被改變。
2)as-if-serial語義創造單線程程序幻境:單線程程序是按程序的順序來執行的;happens-before關係創造多線程程序幻境:正確同步的多線程程序是按happens-before關指定的順序來執行的。
Java內存模型規範對數據競爭的定義:
在一個線程中寫一個變量,在另外一個線程讀同一個變量,並且寫和讀沒有經過同步來排序。
JMM容許編譯器和處理器只要不改變程序執行結果,包括單線程程序和正確同步的多線程程序,怎麼優化都行。
常見的處理器內存模型比JMM要弱,Java編譯器在生成字節碼時,會在執行指令序列的適當位置插入內存屏障來限制處理器的重排序。各類處理器內存模型的強弱不一樣,JMM在不一樣處理器中插入的內存屏障的數量和種類也不相同。
1)lock(鎖定):做用於主內存的變量,把一個變量標識爲一條線程獨佔的狀態。
2)unlock(解鎖):做用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,解鎖後的變量才能夠被其餘線程鎖定。
3)read(讀取):做用於主內存的變量,把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load操做使用。
4)load(載入):做用於工做內存的變量,把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
5)use(使用):做用於工做內存的變量,把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到的變量的值的字節碼指令時將會執行這個操做。
6)assign(賦值):做用於工做內存的變量,把一個從執行引擎收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
7)store(存儲):做用於工做內存的變量,把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
8)write(寫入):做用於主內存的變量,把store操做衝工做內存中獲得的變量的值放入主內存的變量中。
1)不容許read和load、store和write操做之一單獨出現,即不容許一個變量從主內存讀取了但工做內存不接受,或者從工做內存發起回寫了但主內存不接受的狀況出現。
2)不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回主內存。
3)不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從線程的工做內存同步回主內存中。
4)一個新的變量只能在主內存中「誕生」,不容許在工做內存中直接使用一個未被初始化的變量,換句話說,對一個變量實施use、store操做以前,必須先執行過了assign和load操做。
5)一個變量在同一時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。
6)若是對一個變量執行lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值。
7)若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許去unlock一個被其餘線程鎖定的變量。
8)對一個變量執行unlock操做以前,必須先把此變量同步回主內存中。
語義:
線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所作修改的)消息。
線程B獲取一個鎖,實質上是線程B接收了以前某個線程發出的(在釋放這個鎖以前對共享變量所作修改的)消息。
線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A經過主內存向線程B發送消息。
實現:
見AQS等。
語義:
線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發出了(其對共享變量所作修改的)消息。
線程B讀一個volatile變量,實質上是線程B接收了以前某個線程發出的(在寫這個volatile變量以前對共享變量所作修改的)消息。
線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A經過主內存向線程B發送消息。
JMM實現:
表現:
對於final域,編譯器和處理器要遵照兩個重排序規則:
編譯器final語義具體實現:
public class FinalExample { int i; final int j; static FinalExample obj; public FinalExample(){ i = 1; j = 2; // final域 StoreStore屏障 在這裏 // 確保構造函數return前 final域 j=2 完成 } public static void write(){ obj = new FinalExample(); } public static void reader(){ FinalExample object = obj; int a = object.i; // final域 LoadLoad屏障 在這裏 // 確保初次讀對象包含的final域前 讀對象引用完成 int b = object.j; } }
參考文獻《深刻理解Java 虛擬機》、《Java併發編程的藝術》