關於指令重排序的概念,比較複雜,很差理解。咱們從一個例子分析:html
public class SimpleHappenBefore { /** 這是一個驗證結果的變量 */ private static int a=0; /** 這是一個標誌位 */ private static boolean flag=false; public static void main(String[] args) throws InterruptedException { //因爲多線程狀況下未必會試出重排序的結論,因此多試一些次 for(int i = 0; i < 1000; i++){ ThreadA threadA=new ThreadA(); ThreadB threadB=new ThreadB(); threadA.start(); threadB.start(); //這裏等待線程結束後,重置共享變量,以使驗證結果的工做變得簡單些. threadA.join(); threadB.join(); a = 0; flag = false; } } static class ThreadA extends Thread{ public void run(){ a = 1; flag = true; } } static class ThreadB extends Thread{ public void run(){ if(flag){ a = a * 1; } if(a == 0){ System.out.println("ha,a==0"); } } } }
一個簡單的展現Happen-Before的例子.
這裏有兩個共享變量:a和flag,初始值分別爲0和false.在ThreadA中先給a=1,而後flag=true.java
若是按照有序的話,那麼在ThreadB中若是if(flag)成功的話,則應該a=1,而a=a*1以後a仍然爲1,下方的if(a==0)應該永遠不會爲真,永遠不會打印.算法
但實際狀況是:在試驗100次的狀況下會出現0次或幾回的打印結果,而試驗1000次結果更明顯,有十幾回打印.編程
在虛擬機層面,爲了儘量減小內存操做速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照本身的一些規則(這規則後面再敘述)將程序編寫順序打亂——即寫在後面的代碼在時間順序上可能會先執行,而寫在前面的代碼會後執行——以儘量充分地利用CPU。緩存
拿上面的例子來講:假如不是a=1的操做,而是a=new byte[1024*1024](分配1M空間),那麼它會運行地很慢,此時CPU是等待其執行結束呢,仍是先執行下面那句flag=true呢?顯然,先執行flag=true能夠提早使用CPU,加快總體效率,固然這樣的前提是不會產生錯誤(什麼樣的錯誤後面再說)。雖然這裏有兩種狀況:後面的代碼先於前面的代碼開始執行;前面的代碼先開始執行,但當效率較慢的時候,後面的代碼開始執行並先於前面的代碼執行結束。無論誰先開始,總以後面的代碼在一些狀況下存在先結束的可能。安全
無論怎麼重排序,單線程程序的執行結果不能被改變。編譯器、運行時和處理器都必須遵照「as-if-serial」語義。拿個簡單例子來講,多線程
public void execute(){ int a = 0; int b = 1; int c = a+b; }
這裏a=0,b=1兩句能夠隨便排序,不影響程序邏輯結果,但c=a+b這句必須在前兩句的後面執行。併發
在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。app
happens-before原則很是重要,它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們解決在併發環境下兩操做之間是否可能存在衝突的全部問題。
happens-before原則定義以下:框架
- 若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。
重排序在多線程環境下出現的機率仍是挺高的,在關鍵字上有volatile和synchronized能夠禁用重排序,除此以外還有一些規則,也正是這些規則,使得咱們在平時的編程工做中沒有感覺到重排序的壞處。
若是不符合以上規則,那麼在多線程環境下就不能保證執行順序等同於代碼順序,也就是「若是在本線程中觀察,全部的操做都是有序的;若是在一個線程中觀察另一個線程,則不符合以上規則的都是無序的」,所以,若是咱們的多線程程序依賴於代碼書寫順序,那麼就要考慮是否符合以上規則,若是不符合就要經過一些機制使其符合,最經常使用的就是synchronized、Lock以及volatile修飾符。
上面八條是原生Java知足Happens-before關係的規則,可是咱們能夠對他們進行推導出其餘知足happens-before的規則:
happen-before原則是JMM中很是重要的原則,它是判斷數據是否存在競爭、線程是否安全的主要依據,保證了多線程環境下的可見性。
volatile
至關於synchronized
的弱實現,相似於synchronized
的語義,可是沒有鎖機制。在JDK及開源框架隨處可見,可是在JDK6以後synchronized
關鍵字性能被大幅優化以後,幾乎沒有使用了場景。
第一條語義:JMM不會對volatile
指令的操做進行重排序。這個保證了對volatile變量的操做時按照指令的出現順序執行的。
第二條語義是保證線程間變量的可見性,簡單地說就是當線程A對變量X進行了修改後,在線程A後面執行的其餘線程能看到變量X的變更,更詳細地說是要符合如下兩個規則:
雖然volatile
字段保證了可見性,可是因爲缺乏同步機制,因此volatile的字段的操做不是原子性的,並不能保證線程安全。
一般應用場景以下:
volatile boolean done = flase; //... while(!done){ // ... }
獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會致使其它全部須要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另外一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。
CAS 操做包含三個操做數——內存位置(V)、預期原值(A)和新值(B)。若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。不然,處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了「我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。」
一般將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來得到新值 B,而後使用 CAS 將 V 的值從 A 改成 B。若是 V 處的值還沒有同時更改,則 CAS 操做成功。
CAS其底層是經過CPU的1條指令來完成3個步驟,所以其自己是一個原子性操做,不存在其執行某一個步驟的時候而被中斷的可能。
從性能角度考慮:
若是使用鎖來進行併發控制,當某一個線程(T1)搶佔到鎖以後,那麼其餘線程再嘗試去搶佔鎖時就會被掛起,當T1釋放鎖以後,下一個線程(T2)再搶佔到鎖後而且從新恢復到原來的狀態大約須要通過8W個時鐘週期。
假設咱們業務代碼自己並不具有很複雜的操做,執行整個操做可能就花費3-10個時鐘週期左右,那麼當咱們使用無鎖操做時,線程T1和線程T2對共享變量進行併發的CAS操做,假設T1成功了,T2最多再執行一次,它執行屢次的所消耗的時間遠遠小於因爲線程所掛起到恢復所消耗的時間,所以無鎖的CAS操做在性能上要比同步鎖高不少。
示例代碼:
public class SimulatedCAS { private int value; public synchronized int getValue() { return value; } public synchronized int compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (value == expectedValue) value = newValue; return oldValue; } }
非阻塞算法:一個線程的失敗或者掛起不該該影響其餘線程的失敗或掛起的算法。
基於CAS的併發算法稱爲非阻塞算法,CAS 操做成功仍是失敗,在任何一種狀況中,它都在可預知的時間內完成。若是 CAS 失敗,調用者能夠重試 CAS 操做或採起其餘適合的操做。下面顯示了從新編寫的計數器類來使用 CAS 替代鎖定:
public class CasCounter { private SimulatedCAS value; public int getValue() { return value.getValue(); } public int increment() { int oldValue = value.getValue(); while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue) oldValue = value.getValue(); return oldValue + 1; } }
不管是直接的仍是間接的,幾乎 java.util.concurrent 包中的全部類都使用原子變量,而不使用同步。相似 ConcurrentLinkedQueue 的類也使用原子變量直接實現無等待算法,而相似 ConcurrentHashMap 的類使用 ReentrantLock 在須要時進行鎖定。而後, ReentrantLock 使用原子變量來維護等待鎖定的線程隊列。
CAS策略有以下須要注意的事項: