離上次寫博客又隔了好久,心中有愧。在我不斷使用Java的過程當中,幾乎都是拿來就用,就Java併發這塊我尚未系統的梳理過,趁着國慶有空餘時間,把它梳理一遍。如下部份內容參考相關書籍,以做學習之用,特此說明。java
隨着科技的發展,集成電路上的晶體管數量也達到了物理極限,摩爾定律也隨之再也不那麼有效,例如Amdahl定律和Gustafson定律代替它成爲計算機性能發展的源動力。從這個演變也能夠看出,計算機的性能發展也不得不從追求處理器頻率到多核並行處理的發展過程。數組
所謂阿姆達爾(Amdahl)定律,它定義了串行系統並行化後的加速比的計算公式和理論上限。緩存
就是其公式就是:
其中Sp就是加速比,T1是優化前系統耗時,Tp是優化以後系統耗時,p就是處理器個數。那麼這個公式意義就是 加速比 = 優化前系統耗時 / 優化後系統耗時。
咱們逐步看一下它的公式推導:
其中,p爲處理器個數,F爲串行比率,那麼1-F就是並行的比例了。這個公式就是計算優化後的耗時公式,將這個公式代入加速比公式咱們就能夠得出CPU的處理器數量越多,那麼加速比與系統的串行率就成反比:
咱們不妨看個例子,假設如今有個系統是按以下方式串行運行的:
併發
這個系統有三步,其中第一步和第三步都是100ms,第二步是200ms,整個串行的運行時間是400ms。那咱們如今可能要對這個系統作個優化,已知這個系統是兩個核心,那麼若是Step2的操做內部由串行改成並行,那麼理想狀況多是這樣的:
app
咱們看到Step2分解成並行的操做,那麼代入公式獲得最終它的加速比爲1.2。咱們不妨推算一下,假設處理器的個數爲無窮大,那麼Step2的操做耗時無限趨近於0,那麼對於這個系統而言,它的加速比(300ms/200ms)最大也不過是1.5。也就是說,P越趨近於無窮大,那麼Sp=1/F。
加速比越高,代表優化效果越好。根據Amdahl定律,使用多核的CPU對系統優化,優化的效果取決於CPU的數量和系統串行化程序的比重,若是僅僅提高Cpu數量,而不下降程序的串行比重,也是沒法提升系統性能的。因此,咱們要根本上去改變程序的串行行爲,合理的並行與增長處理器數量,才能得到更大的性能提高。性能
Gustafson定律只是從不一樣的角度去闡述處理器個數、串行比例和加速比之間的關係。因此這裏再也不贅述。學習
提升計算機的性能並非讓計算機同時處理多個任務那樣簡單,處理器須要和內存交互,例如讀取數據、存儲運算結果,由於現代計算機的處理器能力太強,存儲設備的讀寫速度與之相差太大,因此在存儲設備和處理器之間加上高速緩存來做爲處理器和內存之間的緩衝。這樣的話CPU就不須要等待相對而言緩慢的內存讀寫了。
當高速緩存做爲一種解決處理器與內存讀寫速度矛盾的手段時,帶來了新的問題,那就是緩存一致性。處理器有對應的高速緩存,而它們又對應同一塊主內存。當多個處理器的運算都涉及到同一個主內存時,該如何保證數據的一致性?因此爲了解決一致性,又在處理器訪問緩存時候遵循一些協議。
那麼諸如Java虛擬機的內存模型之類就能夠理解成,在特定的操做協議下,對特定的內存或者高速緩存進行讀寫的過程抽象。
除了高速緩存以外,處理器也會對輸入代碼亂序執行優化(Out-Of-Order Execution)優化,這種優化並不能保證處理器的執行順序會和輸入代碼的順序一致,但會保證最終輸入的結果是一致的。與之相對應的,Java也存在着一套相似的機制,就是指令重排(Instruction Reorder)優化。
優化
Java虛擬機定義了一套內存模型來屏蔽各類硬件和操做系統帶來的內存訪問差別,實現Java程序在各平臺下達到一致的內存訪問結果。
JMM主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲、取出的底層細節。這裏的變量包括實例字段、靜態字段和構成數組對象的元素,但不包括局部變量和方法參數,由於這些是線程私有的,不會被共享。
Java內存模型規定全部變量都存在主內存,每條線程都有本身的工做內存,線程全部對變量的操做都必須在工做內存中執行,線程的工做內存保存了被該線程使用到的變量主內存拷貝,不能直接讀寫主內存中的變量,線程之間變量值傳遞須要經過主內存完成。線程、主內存、工做內存交互以下:
this
在主內存和工做內存之間的交互協議的具體細節,Java內存模型定義了8個操做來完成,虛擬機來保證這8個操做都是原子的。atom
操做 | 說明 | 描述 |
---|---|---|
lock | 鎖定 | 做用於主內存的變量,將一個變量標識爲一條線程獨佔的狀態 |
unlock | 解鎖 | 做用於主內存的變量,將一個標記爲鎖定狀態的變量解鎖,以便其它線程使用 |
read | 讀取 | 做用於主內存的變量,將一個變量的值從主內存傳輸到線程的工做內存中,以便load操做使用 |
load | 載入 | 做用於工做內存的變量,將read操做讀取過來的值放入工做內存的變量副本中 |
use | 使用 | 做用於工做內存的變量,將工做內存的值傳遞給執行引擎,虛擬機遇到一個須要使用變量的字節碼指令就會這麼作 |
assign | 賦值 | 做用於工做內存的變量,從執行引擎接受到的值賦給工做內存的變量,虛擬機遇到一個給變量賦值的字節碼指令就會這麼作 |
store | 存儲 | 做用於工做內存的變量,將工做內存的變量的值傳遞給主內存中,以便write操做 |
write | 寫入 | 做用於主內存的變量,它把store操做從工做內存中獲得的變量賦值放入主內存的變量中 |
Java內存模型只要求兩個操做必須按順序執行,而沒有保證是連續執行,也就是說兩個指令之間是能夠有其它指令的。Java內存模型還規定可在執行上述8種基本操做時必須知足如下的規則:
* 不容許read和load、store和write操做之一單獨出現;
* 不容許一個線程丟棄它最近的assign操做,即assign操做以後必須將值同步給主內存;
* 不容許一個線程沒發生過assign就把數據同步給主內存;
* 一個新的變量只能誕生在主內存,不容許工做內存直接使用一個未被(load和assign)的變量;
* 一個變量在同一時刻只容許同一條線程對其進行lock操做;
* 若是對一個變量執行lock,那麼將清空這個變量在工做內存的此變量的值,在執行引擎使用這個變量以前,從新執行load和assign操做初始化工做內存的值;
* 若是一個變量事先沒有被lock操做鎖定,就容許對其或其它線程進行unlock操做;
* 對一個變量執行unlock操做以前,必須先同步回主內存;
原子性(Atomicity):原子性是指一個操做是不可中斷的,一旦一個操做開始,就不會被其它線程干擾。Java內存模型來直接保證原子性變量的操做包括read、load、assign、use、store和write,基本能夠認爲基本數據類型的讀寫是原子性的,可是double、long類型例外,這是它們的非原子性協定決定的。固然,Java內存模型還提供了lock和unlock來知足更大範圍的原子性操做,這兩個操做反映到字節碼指令就是monitorenter和monitorexit隱式的操做,反映到代碼上就是synchronized關鍵字。
可見性(Visibility):可見性是指一個線程修改了共享變量的值,其它線程能當即得知這個更改。Java內存模型是經過變量修改後將新值同步給主內存,在變量讀取前從主內存刷新變量值依賴主內存做爲傳遞媒介的方式來實現可見性的,不管這個變量是否被volatile修飾,但它們的區別是volatile變量的特殊規則能當即同步到主內存,以及每次使用前從主內存刷新,而普通變量不行。固然,除了volatile能實現可見性以外,synchronized和final一樣能夠。synchronized的可見性是經過「對一個變量執行unlock操做以前,必須把此變量同步回主內存中」這條規則得到的;而final的可見性是指,被final修飾的字段在構造器一旦初始化完成,而且構造器沒把this的引用傳遞出去,那麼在其它線程就能看見final字段的值。
有序性(Ordering):前面也提到,java會指令重排,代碼順序未必和指令執行順序一致。Java提供了volatile和synchronized來保證線程之間操做的有序性。volatile關鍵字自己就禁止指令重排,而synchronized是由「一個變量在同一時刻只容許一條線程對其lock操做」這條規則得到。
Java裏的有序性除了靠volatile和synchronized兩個關鍵字完成,其實還隱藏着先行發生(Happen-Before)原則,經過這個原則和以前的規則基本能解決併發環境下兩個操做之間的衝突問題。
* 程序次序原則(Program Order Rule):一個線程內保證語義的串行;
* 管程鎖定原則(Monitor Lock Rule):unlock操做一定在以後的同一個鎖的lock操做以前;
* volatile規則(Volatile Variable Rule):volatile變量的寫操做先行發生於後面這個變量的讀操做;
* 線程啓動規則(Thread Start Rule):線程的start()方法先於該線程其它的每個動做;
* 線程終止規則(Thread Termination Rule):線程的全部操做都先於該線程的終結(Thread.join());
* 線程中斷規則(Thread Interruption Rule):線程的interrupt()方法調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
* 對象終結規則(Finalizer Rule):一個對象的初始化完成先行與它的finalize()方法的開始;
* 傳遞性 (Transitivity):若是A操做先於B操做,B操做先於C操做,那麼A一定先於C;
Java內存模型基本是圍繞原子性、有序性、可見性展開,而volatile關鍵字的語義,一是保證此變量對全部線程的可見性,二是禁止指令重排。能夠看出,volatile不能保證原子性,這個須要經過加鎖或者一些原子類來實現。
舉個例子:
public class VolatileTest { public static volatile int i = 0; public static void increase() { i++; } public static class IncreaseTask implements Runnable{ public void run() { for (int y = 0; y < 10000; y++) { increase(); } } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new Thread(new IncreaseTask()); threads[i].start(); } for (int i = 0; i < 10; i++) { threads[i].join(); } System.out.println(i); } }
在上面這段代碼中,變量i用volatile修飾,循環10個線程,每一個線程內部對i遞增10000次,若是這段代碼併發成功的話,預期的結果應該是100000。可是運行結果可見,每次的結果值都小於100000。
這個正是由於increase()方法內部對i遞增的處理,也就是 i++ 這一段代碼不是原子的,代碼雖然只有一行,可是編譯出來的字節碼指令卻有多個指令,並且每一個指令自己未必就是原子的,由於這些指令還會轉化成若干個本地機器碼指令。不難分析出,每一個線程取到i的值那一刻,volatile保證了這一刻取到的是正確的數據,可是繼續往下執行的時候,這個值就可能已經被其它線程修改了,而此時的數據就變成過時的數據,同步到主內存中的數據就多是一個較小的數據。
除了在操做遞增時候加鎖以外,使用AtomicInteger原子類代替int同樣能夠獲得預期的結果。
volatile修飾的變量,賦值後的指令會多出一個內存屏障,這個內存屏障會杜絕後面的指令排到前面去。這種內存屏障其實就是一個空操做,這個空操做指令是lock前綴,它的做用就是使得本CPU的Cache寫入內存(write和store操做),該寫入動做使得其它CPU或者別的內核無效化其Cache,因此經過這樣的一個空操做,讓volatile修飾的變量對其它CPU當即可見。也所以,這個空操做指令在同步到內存時,意味着全部的操做都已經執行完成,這樣就造成了「指令排序沒法越過屏障」的效果。