深度剖析synchronized、volatile的實現細節

線程

  • 什麼是線程?

    線程(thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務

  • 什麼是併發?

    當多個線程同時執行相同的控制流時,我們就稱之爲併發;用代碼語言來說,就是多個線程同時使用某個類,調用某個方法,修改某個變量。

  • java中創建線程的幾種方式

    public class Test
    {
        public static void main(String[] args) throws Exception
        {
            // 第一種,實例化一個繼承自Thread的類,調用start方法
            new MyThread().start();
    
            // 第二種,實例化一個Thread並傳遞一個Runnable對象,調用start方法
            new Thread(new MyRunnable()).start();
    
            // 第三種,實例化一個Thread,傳遞一個FutureTask對象,FutureTask對象中傳遞Callable,調用start方法
            //       Callable的結果會在未來某個時間返回給FutureTask
            //       可以通過FutureTask的get方法獲取到
            FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
            new Thread(futureTask).start();
            Thread.sleep(10);
            System.out.println(futureTask.get());
    
            // 第四種,基於lambda的匿名內部類
            // lambda表達式,java8纔出現
            new Thread(() ->
            {
                String va = "lambda...";
                System.out.println(va);
            }).start();
    
            // 第五種,基於線程池的方式,但是其線程的本質與上面的幾種沒有實質性的區別
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(() ->
            {
                System.out.println("thread pool...");
            });
            executorService.shutdownNow();
        }
    }
    
    //以下是上面測試方法相關的測試對象
    public class MyThread extends Thread
    {
        @Override
        public void run()
        {
            System.out.println("my thread...");
        }
    }
    
    public class MyRunnable implements Runnable
    {
        @Override
        public void run()
        {
            System.out.println("my runnable...");
        }
    }
    
    public class MyCallable implements Callable<String>
    {
        @Override
        public String call() throws Exception
        {
            System.out.println("my callable...");
            return "callable return...";
        }
    }
    
  • 什麼是線程安全?

    當系統出現高併發,多個線程執行某個方法,或者修改某個變量的時候,如果不考慮併發問題,可能因爲執行的時序從而導致各個線程間的值交叉錯亂;如下單例的示例

    public class SingleObj
    {
        private static SingleObj singleObj = null;
        
        private SingleObj(){}
        
        public static SingleObj getInstance(){
            // 假如有10個線程同時執行到這個位置
            // 那麼這10個線程的 if(null == singleObj) 判斷的都會是ture
            // 因此這10個線程就都會走if內的實例化
            // 從而導致,系統中singleObj就不是單例對象了
            if(null == singleObj){
                singleObj = new SingleObj();
            }
            return singleObj;
        }
    }
    
  • 線程安全產生的原因

    • 存在共享數據
    • 多線程同時操作共享數據
    • 緩存數據
  • 解決線程安全,線程同步的手段

    • 編碼習慣

      避免不必要的共享數據,保證堆上的所有數據不會逃逸出去從而被其他的線程訪問

    • 加鎖;如:synchronized

      將一個對象鎖住,保證同一時間只有拿到鎖的線程在執行

    • ThreadLocal

      線程局部變量,以空間換時間,各線程將變量保存在私有的內存中

    • volatile

      保證數據的可見性,防止指令重排

    • final

      數據只讀,不能寫

對象的內存佈局

在整理線程安全相關東西之前,我們來了解一下,一個對象在HotSpot虛擬機中的內存佈局;

  • 對象組成部分

    • 對象頭(Mark Word)
    • 類型指針(Class Pointer) 這部分數據是否存在取決去虛擬機的實現
    • 實例數據 (Instance Data)
    • 對齊填充(Padding)

    file

對象頭

HotSpot對象頭主要用於存儲運行時數據(HashCode [ identity ],GC分代年齡,鎖狀態標記、偏向線程ID,偏向時間戳等),這部分數據隨着當前對象的鎖狀態,不斷的在發生變化,如下圖所示;在32位和64位的虛擬機中,這部分數據分別爲32bit和64bit
file

類型指針

指向這個類元數據的指針,比如,這個對象是一個Object,那麼這個指針就指向Object,虛擬機通過這個指針來確定這個對象是那個類的實例;不過並不是所有的虛擬機實現都必須在對象數據上保留類型指針,因此,查詢對象的元數據並不一定要經過對象本身,所以,對象的訪問取決於虛擬機的實現,可以是通過句柄的方式,也可以是通過直接指針的方式;

  • 句柄
    如果使用句柄的話,那麼java堆中會劃分一部分內存出來用於做句柄池,reference中存儲的就是對象的句柄地址,句柄地址包含了實例數據和類型數據的具體地址信息。如下圖:
    file
  • 直接指針
    如果使用直接指針,那麼在java堆對象的佈局中就必須放置類型數據的相關信息,而reference中存儲的就是直接的對象地址
    file
  • 優缺點對比
    句柄方式多一次尋址的開銷,因此,在對象的訪問速度上,沒有直接指針快;但是在GC導致對象發生移動的時候,句柄的方式只需要修改句柄中實例數據的指針,而reference是不需要做任何調整的。

實例數據

真正存儲有效信息的區域,也就是程序代碼中所定義的各種類型的字段內容。無論是從父類中繼承的,還是子類中定義的,都會在這裏記錄起來。這部分的數據的順序受虛擬機參數配置和字段在java代碼中定義的順序影響。HotSpot的分配策略爲:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers 普通對象指針),可以看出,相同寬度被分配在一起。在滿足這個前提下,父類定義的變量會出現在子類之前。如果CompactFlashs參數值爲true(默認爲true),那麼子類較窄的變量也可能會插入到父類變量的空隙之中

對齊填充

這部分數據並不是必然存在的,同時這部分數據也沒有什麼實質性的含義,僅僅起到了佔位符的作用;是因爲HotSpot虛擬機要求對象必須是8的整數倍;因此,如果不夠的情況下,需要進行填充補齊。


瞭解了對象在HotSpot虛擬機中的的一個基本結構之後,便於下面去分析一個對象在鎖的時候,數據發生了一些說明樣的變化


synchronized

  • 示例代碼
    public class SynchronizedDemo
    {
        private Object object = new Object();
        public void m(){
            synchronized (object){
                System.out.println("123");
            }
        }
    }
    
    基本概念:synchronized用於去鎖一個對象,保證同一時間只有一個線程會拿到並持有鎖,拿到鎖的線程允許執行synchronized包裝的代碼塊;記住synchronized鎖的是對象,並不是鎖的代碼塊;如上面的代碼所示,鎖的是object對象,執行的是synchronized後面{}內的代碼,以上示例System.out.println("123");永遠只會有一個線程調用它。
  • 字節碼信息
    下圖左側是帶synchronized的字節碼,右側是不帶的字節碼
    file

上圖可以看出,左側核心的區別是多了monitorenter和monitorexit兩個關鍵字,在這段代碼直接的數據,就只能有一個線程同時訪問。字節碼信息查看可以下載一個插件jclasslib Bytecode viewer

  • 鎖的演變過程
// 再次那這個圖拿出來,通過這個圖,來分析一下整個對象鎖的演變過程

file

// 下圖爲整個鎖狀態變化的過程

file

  • 無鎖態;

    當一個對象剛剛創建(new Object())出來的時候,這個對象是一個嶄新的,因此他屬於無鎖狀態;此時對象頭部分保存的是對象的hashcode等信息

  • 偏向鎖

    • 當一個無鎖態的對象由第一個線程來使用(糟蹋)它的時候;
    • 虛擬機將對象頭中的鎖標識更新爲"01”
    • 虛擬機通過CAS操作將線程ID記錄到對象的Mark Word中;
    • 持有偏向鎖的線程以後每次進入這個鎖相關的代碼塊,虛擬機都不需要進行同步操作(例如Locking、Unlocking及對Mark Word的update等);正是因爲其偏袒着這個的線程,所以稱其爲偏向鎖
    • 當有另外一個線程去嘗試獲取這個鎖的時候,偏向模式宣佈結束;根據當前對象是否處於鎖的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(“01”)狀態或輕量級鎖狀態
      file
  • 輕量級鎖(自旋鎖)

    • 當此時有多個線程同時獲取偏向鎖會升級爲輕量級鎖;
    • 當同步對象的鎖狀態爲01(未鎖定)的時候,虛擬機首先會在當前的棧中創建一個爲鎖記錄(Lock Record)的空間,用來存儲當前對象Mark Word的拷貝,同時保存當前鎖記錄的擁有者(owner)是誰;
    • 然後,操作系統通過CAS修改對象的Mark Word數據,將鎖記錄(Lock Record)的指針記錄到Mark Work(while(true){} 這麼個意思)
      file
  • 重量級鎖

  • 當對象的自旋次數超過了10次(可以通過-XX:PreBlockSpin修改),或者CPU內核的1/2;此時鎖就會驚動聖上(OS),升級爲重量級鎖

  • java的線程是映射到操作系統的原生線程之上的,如果要阻塞或喚醒一個線程,都需要操作系統來幫忙完成。

  • 當切換到重量級鎖的時候,就需要從用戶態切換到內核態,線程會被掛起,因此狀態轉換需要消耗很多的處理時間,這個就是其重的原因

  • 重量級鎖的思考;既然有輕量級鎖,爲什麼還要存在重量級鎖呢?原因主要是因爲輕量級鎖是通過自旋來實現的,當出現大量的鎖競爭的時候,無任何意義的自旋操作會大量佔用CPU,從而導致性能的下降。

  • 其他鎖相關的概念

    • 鎖消除
      鎖消除是指虛擬機編譯器在運行時,對一些代碼上需要同步,但是被檢測到不可能存在共享數據的競爭進行消除。鎖消除的主要依據是來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據不會逃逸出去從而被其他的線程訪問,那就可以把他們當做是棧上面的數據,從而認爲你他們是線程私有的,同步加鎖自然就沒有說明意義;可能會想,這個過程,我們只需要在寫代碼的時候注意一下就好了,但是很多時候,同步的代碼並不是全部由程序員控制的,比如下面的代碼:

      public void strAppand(String s1, String s2)
      {
          StringBuffer sb = new StringBuffer();
          sb.append(s1).append(s2);
      }
      

      這段代碼意思很簡單,而且也很常用;開發過程中也經常會出現字符串拼接的操作,如:s1+s2,最終虛擬機對String拼接會按以上的方式進行優化,但是我們知道StringBuffer是線程安全的,因此他的append方法是有synchronized修飾的,那麼sb的append操作難道都對StringBuffer加鎖了嗎?其實不然,通過上面的代碼我們發現,append的作用域都限制在strAppand()的方法內部,那麼方法內的所有引用永遠不會“逃逸”出strAppand()內部;這樣一來,雖然說sb的每個操作都加了鎖,但是可以被安全的消除,在即時編譯的之後,這段代碼會忽略掉所有的同步直接執行。

    • 鎖細化
      在編寫代碼的時候,我們要儘量保證同步代碼塊的作用域小,只有在有共享數據的實際作用域去進行同步,這樣使得需要同步的操作數量儘量減小,如果存在競爭,那麼等待鎖的線程也能儘快拿到鎖,下面以雙重檢查代理爲例:

      public class SingleObj
      {
          private volatile static SingleObj singleObj = null;
      
          private SingleObj(){}
      
          public static SingleObj getInstance(){
              if(null == singleObj){
                  synchronized (SingleObj.class){
                      if(null == singleObj){
                          singleObj = new SingleObj();
                      }
                  }
              }
              return singleObj;
          }
      }
      

      我們共享的數據只有singleObj這一個對象,因此,我們只需要將同步代碼塊放在singleObj是否等於空的校驗上,這樣,一旦這個單例對象創建成功之後,同步代碼塊就不會再執行,因此就可以提高整個代碼的執行效率及性能;下面是簡單粗暴的方式:

      public class SingleObj
      {
          private volatile static SingleObj singleObj = null;
      
          private SingleObj(){}
      
          public synchronized static SingleObj getInstance(){
              if(null == singleObj){ 
                  singleObj = new SingleObj();
              }
              return singleObj;
          }
      }
      

      同步代碼塊直接加載方法上,雖然也能確保線程安全,但是,這樣的實現導致就算單例對象以及被實例化,每次通過getInstance()方法獲取對象的時候,都會走加鎖獲取,代碼性能會大大的下降。

    • 鎖粗化
      這個和上面的鎖細化是一個反的概念,既然上面說了鎖細化的概念,也提倡使用鎖細化,但是這裏爲什麼又搬出來一個鎖粗化的概念,因爲凡事都有兩面性,我們要根據實際的情況去做相應的調整,如下代碼:

      public class NumOpe
      {
          private static int num = 0;
          public static void m()
          {
              for (int i = 0 ; i < 10 ; i++){
                  synchronized (NumOpe.class){
                      num++;
                  }
              }
          }
      }
      

      當我們對num進行循環追加的,希望能夠保證num的線程安全,不會因爲併發導致加錯,如果按以上的方式加鎖的,每一次循環都會有一個加鎖及釋放的過程;但是我們的目的是每次for循環追加10個數,因此,我們將鎖同步的範圍擴大(粗化)到整個操作序列的外部,完全可以對同步代碼塊進行以下的方式粗化:

      public static void m()
      {
         synchronized (NumOpe.class){
               for (int i = 0 ; i < 10 ; i++){
                 num++;
               }
           }
      }
      

      這樣既可以線程安全,同時也不會因爲粒度太細而導致性能的下降;類似於StringBuffer的appand方法亦是如此。

    • 自適應自旋鎖
      在JDK1.6中引入了自適應的自旋鎖。自適應意味着自旋的時間不再固定了,而是由前-次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100個循環。另外,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越準確,虛擬機就會變得越來越“聰明”了。

  • 總結

volatile

  • volatile解決了什麼問題?
    • 可見性
    • 防止指令重排

可見性的問題

  • 案例分析

    public class VolatileTest
    {
        public static void main(String[] args) throws Exception
        {
            Mythread mythread = new Mythread();
            mythread.start();
    
            Thread.sleep(100);
            mythread.stopMe();
        }
    }
    
    class Mythread extends Thread
    {
        private boolean label = false;
    
        public void stopMe()
        {
            label = true;
        }
    
        @Override
        public void run()
        {
            System.out.println("start");
            while (!label)
            {
            }
            System.out.println("stop");
        }
    }
    

    如上的演示代碼,當main方法跑起來之後,是否能夠正常退出?答案是隻會打印start就陷入到了while的死循環,永遠不會退出。但是按代碼邏輯來看,線程啓動100ms之後就調用了stopMe(),爲什麼這個停止的沒有生效呢?在瞭解原因之前,我們得先了解一下關於緩存方面的一些知識。

  • 硬件的高速緩存
    基於線程,我們可以在同一個計算機上面去執行多個任務;這樣的目的是爲了儘可能多的去“壓榨”計算機,讓其儘可能多的發揮其作用,但是並不是所有的任務都只是去計算;更多的任務是處理器計算,保存到內存,然後進行反覆的IO操作得到一個協同的最終結果;由於處理器相比於內存運算速度相差了幾個數量級,爲了解決這個大的差異帶來的性能問題,在處理器和內存之間加一個高速內存,這個高速內存的目的是用戶處理器和內存之間的緩衝;將運算要使用到的數據拷貝到高速緩存中,讓運算能夠快速進行,當運算結束之後,將緩存中的數據同步到內存中,很好的解決了處理器與內存之間的速度矛盾。
    file
    file

  • java的工作內存
    java的內存模型規定了所有的變量都保存在主內存中,但當前線程創建之後,每條線程有自己的工作內存,線程的工作內存保存了被該線程使用到的變量的主內存拷貝,線程中對變量的所有操作(讀取、賦值等)都必須在工作線程完成,而不能直接操作主內存;不同線程之間不能相互訪問工作內存中的變量,各個工作內存之間需要交互數據的話只能通過主內存。
    file

  • 帶來的問題
    上面的緩存與工作內存確實給機器的性能,數據的隔離帶來了很大的幫助,但是卻帶來了一個新的問題:數據的可見性;當一個由多個線程共同維護的變量,由於緩存的存在導致各個線程之間的修改並不透明,無法在第一時間得到通知。因此就出現了上面演示代碼中的問題,修改並沒有對其他線程可見。

  • 如何解決可見性問題:volatile
    volatile修飾的變量在各個內存之間都是立即可見的;所有線程的寫操作都會第一時間反應在其他線程中,關於volatile變量的可見性;

  • volatile解決可見性,但是沒辦法保證一致性
    經常會被開發人員誤解,認爲以下描述成立:“volatile 變量對所有線程是立即可見的,對voltile變量所有的寫操作都能立刻反應到其他線程之中,換句話說,volatile 變量在各個線程中是一致的,所以基於volatile變量的運算在併發下是安全的”。這句話的論據部分並沒有錯,但是其論據並不能得出“基於volatile變量的運算在併發下是安全的”這個結論。volatile 變量在各個線程的工作內存中不存在一致性問題(在各個線程的工作內存中,volatile變量也可以存在不一致的情況,但由於每次使用之前都要先刷新,執行引擎看不到不一-致的情況,因此可以認爲不存在一致性問題),但是Java裏面的運算並非原子操作,導致volatile變量的運算在併發下一樣是不安全的,我們可以通過一段簡單的代碼來說明原因:

    public class VolatileTest2
    {
        public static volatile int num = 0;
    
        public static void incr()
        {
            num++;
        }
    
        private static final int THREAD_NUM = 20;
    
        public static void main(String[] args)
        {
            // 開啓20個線程,分別調用incr()對num進行追加
            Thread[] threads = new Thread[THREAD_NUM];
            for (int i = 0 ; i < THREAD_NUM ; i++)
            {
                threads[i] = new Thread(() ->
                {
                    for (int j = 0 ; j < 10000 ; j++)
                    {
                        incr();
                    }
                });
                threads[i].start();
            }
    
            while (Thread.activeCount() > 2)
                Thread.yield();
    
            System.out.println(num);
        }
    }
    

    file

    理論上20個線程,每個調用10000次,最後追加的結果應該是20萬,由上面可以看出,幾乎很小概率能夠正確,那麼原因出現在哪裏?

    // 查看這個方法的字節碼
    public static void incr()
    {
        num++;
    }
    
    // 以下爲字節碼
    0 getstatic #2 <VolatileTest2.num>
    3 iconst_1
    4 iadd
    5 putstatic #2 <VolatileTest2.num>
    8 return
    
    //上面的字節碼可以看出 num++行代碼被編譯成了4條指令
    //getstatic指令將num的值取到棧頂的時候,volatile能保證拿到的值是最新的
    //但是在執行iconst_1、iadd這兩條指令的時候,沒有辦法保證其他的線程不做修改
    

    那麼可以通過synchronized解決這個問題

    // 如果爲了保證原子性使用synchronized,就可以不用volatile
    public synchronized static void incr()
    {
        num++;
    }
    

指令重排

  • 啥是指令重排?
    普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,但是沒有辦法保證變量賦值操作的順序(代碼順序)和程序代碼的執行順序一致。這是JVM對代碼執行的優化,更快的執行

  • 指令重排測試代碼

    public class Disorder
    {
        static int a = 0, b = 0;
        static int x = 0, y = 0;
    
        public static void main(String[] args) throws InterruptedException
        {
            int i = 0;
            for ( ; ; )
            {
                a = b = x = y = 0;
                i++;
                Thread thread1 = new Thread(() ->
                {
                    a = 1;
                    x = b;
                });
                Thread thread2 = new Thread(() ->
                {
                    b = 1;
                    y = a;
                });
    
                thread1.start();
                thread2.start();
                thread1.join();
                thread2.join();
    
                if (x == 0 && y == 0)
                {
                    System.err.println("第" + i + "次異常,X=" + x + " Y=" + y);
                    break;
                }
                else
                {
                    System.out.println("第" + i + "次,X=" + x + " Y=" + y);
                }
            }
        }
    }
    // 上面的代碼可以分析得出x和y值可能出現以下情況
    // 第一種: x=1,y=0
    // 第二種: x=1,y=1
    // 第三種: x=0,y=1
    
    // 如果出現x=0 , y=0的時候,說明發生了指令重排
    
    // 以下的測試結果證明了確實發生了重排
    

    file

  • 另外一個問題場景;DCL(Double Check Lock)單例是否需要加volatile?

    // 我們再次回到雙重檢查的單例
    public class SingleObj
    {
        // 這裏是否需要加volatile?
        private volatile static SingleObj singleObj = null;
    
        private SingleObj(){}
    
        public synchronized static SingleObj getInstance(){
            if(null == singleObj){ 
                singleObj = new SingleObj();
            }
            return singleObj;
        }
    }
    
  • 一個簡單類T的實例化過程來分析

    public class T
    {
        private int i;
    
        public T()
        {
            i = 1;
        }
    
        public static void main(String[] args)
        {
            T t = new T();
        }
    }
    
    //以下是T t = new T();實例化過程的字節碼指令
    
    // 實例化一個T對象 賦初始值 i=0
    0 new #3 <T>
    3 dup
    // 初始化變量  i=1
    4 invokespecial #4 <T.<init>>
    // 將示例對象映射到t
    7 astore_1
    8 return
    
    // 第一步: 實例化對象T,並將變量賦一個初值
    // 第二步:初始化變量的值
    // 第三步:實例化對象與棧裏的引用t之間的建立關聯,此時t就不爲null
    
  • 模擬一個指令重排的過程

    // 如果現在的字節碼指令的第二步和第三步發送了重排,執行順序如下:
    0 new #3 <T>
    3 dup
    7 astore_1
    // 假如說單例的第一個線程執行到了這裏,另外的併發線程進入了
    // 此時堆棧中的t對象以及不爲null了,按我們上面的單例方式,就直接返回了
    // 但是,這會兒這個對象的值並沒有對熟悉進行初始化,對象中i的i值爲0
    4 invokespecial #4 <T.<init>>
    8 return
    

    以上DCL指令重排的問題實在是沒辦法通過測試代碼測試復現出來;這種情況確實出現的概率非常的低,也只有在高併發很大的情況下,極低的可能性出現,而且出現之後也非常的難追蹤;概率低不代表不會出現,因此,我們在寫DCL單例的時候一定要注意這個問題。

  • volatile到底經歷了什麼防止了指令重排?

    • 測試代碼
      public class T
      {
          private volatile int num;
          private int num2;
      
          public int incr(int a, int b)
          {
              int temp = a + b;
              num += temp;
              return temp;
          }
      
          public int incr2(int a)
          {
              num2 += a;
              return num2;
          }
      
          public int getNum()
          {
              return num;
          }
      
          public static void main(String[] args)
          {
              T t = new T();
      
              int sum = 0;
              int sum2 = 0;
              for (int i = 0 ; i < 1000 ; i++)
              {
                  sum = t.incr(sum, 1);
                  sum2 = t.incr2(i);
              }
              System.out.println("num:" + t.getNum());
              System.out.println("sum:" + sum);
              System.out.println("sum2:" + sum2);
          }
      }
      
    • volatile在字節碼中的體現
      // 帶volatile的在字節碼層僅僅表現爲訪問標識不同
      
      file
    • 內存屏障(Memory Fence或者Memory Barrier)
      volatile在字節碼層面僅僅表現爲一個訪問標識的不同,在JVM和操作系統層面,表現爲內存屏障,將volatile修飾的變量與其他的操作通過屏障隔離起來,不允許執行順序發生變化;同時將值的修改立刻對其他線程可見。
      • JVM中的內存屏障
        file
        • StoreStoreBarrier和StoreLoadBarrier

          寫操作時;前面一個寫屏障,後面一個讀屏障;通俗的意思就是說,volatile寫操作的時候,他之前的操作,你先寫,我等着,後面的讀操作,不好意思,等我先寫完你再讀

          • StoreStore屏障
            對於這樣的語句Store1; StoreStore Store2, 在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
          • StoreLoad屏障
            對於這樣的語句Store1; StoreLoad Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫 入對所有處理器可見。
        • LoadLoadBarrier和LoadStoreBarrier

          讀操作時;前面一個讀屏障,後面一個寫屏障;通俗的意思就是說,volatile讀操作的時候,他之前的讀操作,你先讀,我等你讀完,後面的寫操作,不好意思,等我先讀完你再寫

          • LoadLoad屏障
            對於這樣的語句Load1;LoadLoad; Load2,在Load2及後續讀取操作要讀取的數據被訪問前,保證load要讀取的數據被讀取完畢
          • LoadStore屏障
            對於這樣的語句Load1; LoadStore;Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。
      • volatile操作系統(windows)層面的實現
        file
        通過上面的代碼,可以看出,加了volatile和不加在JVM中體現爲一個lock addl指令,加了內存屏障多執行的一條lock addl $0x0,(%rsp)指令;這個操作類似於一堵牆,在重排序的時候,後面的指令不能重排序到內存屏障之前;當只有一個CPU訪問內存的時候,是不需要內存屏障的,但是當多個CPU同時訪問同一塊內存,且其中一個在觀測另外一個,就需要保證一致性;其中addl $0x0,(%rsp)把rsp寄存器中的值增加0,很顯然這個是一個空操作,關鍵的lock前綴會使本CPU的Cache寫入到主存,該寫入當做也會引起其他CPU或者內核無效化(Invalidate)其Cache,這個操作相當於對Cache中的變量做了一次Java內存模型中的“store”和“write”操作。所以通過這樣的一個空操作,可以讓volatile變量的修改對其他CPU可見
      • volatile操作系統(linux x86 CPU)層面的實現
        • sfence:在sfence指令前的寫操作當必須在sfence指令後的寫操作前完成。
        • lfenice: 在Ifence指令前的讀操作當必須在Ifence指令後的讀操作前完成。
        • mfence:在mfence指 令前的讀寫操作當必須在mfence指令後的讀寫操作前完成。

總結

  • synchronized
    通過鎖定對象的方式,保證同一時間只有一個對象對指定代碼塊進行訪問
  • volatile
    通過內存屏障的方式,防止指令的重排序;所有的修改都立即同步主存,同時將其他緩存中的數據實現,保證數據的第一時間的可見性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章