java併發(二)synchronized和volatile

二,synchronized

2.1 臨界區

  • 一個程序運行多個線程本身是沒有問題的
  • 問題出在多個線程訪問共享資源
    • 多個線程讀共享資源其實也沒有問題
    • 在多個線程對共享資源讀寫操作時發生指令交錯,就會出現問題
  • 一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊爲臨界區
static int counter = 0;
 
static void increment() 
// 臨界區 
{ 
    counter++; 
}

2.2 競態條件

多個線程在臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之爲發生了競態條件

所以我們需要避免臨界區的競態條件發生
- 阻塞式解決:lock,synchronized
- 非阻塞式解決:原子變量

2.3 synchronized應用及原理

synchronized也就是對象鎖,他採用互斥的方式,保證同一時刻最多隻有一個線程能持有對象鎖,其他線程想要獲取對象鎖時會被阻塞住,這樣保證只有一個線程能進入到臨界區,安全的執行臨界區的代碼

變量的線程安全分析:

  • 成員變量和靜態變量是否線程安全?
    • 如果變量沒有被共享,則是線程安全
    • 如果他們被共享了又分兩種情況討論:
      • 如果只有讀操作,則線程安全
      • 如果有讀有寫,則線程不安全
  • 局部變量是否線程安全?
    • 局部變量肯定是線程安全的
    • 但是局部變量引用的對象未必線程安全,如果該對象逃離方法的作用範圍,需要考慮線程安全

2.3.1 synchronized應用

synchronized可以加在實例對象,類對象和方法上

加在實例對象,其實和加在實例方法上是一樣的,鎖住的都是當前實例對象

            synchronized (this){
                for (int i = 0; i < 5000; i++) {
                    newCount--;
                }
            }
        public synchronized void sync(){
            newCount++;
        }

類對象

            synchronized (Object.class){
                for (int i = 0; i < 5000; i++) {
                    newCount--;
                }
            }
            
        //synchronized加在靜態方法上,相當於synchronized(B.class),相當於鎖住類對象
        public synchronized static void syncStatic(){

        }

2.3.2 synchronized原理

Java對象頭

對象大致可以分爲3部分組成,對象頭,實例數據和填充字節

以32位虛擬機爲例

在這裏插入圖片描述

在這裏插入圖片描述

  • thread:持有偏向鎖的線程ID。
  • epoch:偏向時間戳。
  • ptr_to_lock_record:指向棧中棧楨鎖記錄的指針。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指針。
  • klass_word:用於存儲對象的類型指針,該指針指向它的類元數據,JVM通過這個指針確定對象是哪個類的實例

鎖標記:

其中biased_lock:對象是否啓用偏向鎖標記,爲1時表示對象啓用偏向鎖,爲0時表示對象沒有偏向鎖。

在這裏插入圖片描述

在這裏插入圖片描述

在無鎖狀態下和偏向鎖下,markword會存儲hashcode等信息,在輕量級鎖狀態下,hashcode等信息在線程私有的棧中棧楨的鎖記錄中,重量級鎖狀態下,位置被鎖指針佔據,hashcode等信息存儲在對象關聯的monitor上

monitor

monitor是一個同步工具,每個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,monitor可以和對象一起創建銷燬,或當線程試圖獲取對象鎖時自動生成,但是當一個monitor被一個線程持有後,monitor中的_owner就會寫上持有線程的id,其他線程就無法獲取到該monitor進入monitor中的_EntryList

其主要數據結構如下:

ObjectMonitor() {
   _header       = NULL;
   _count        = 0;
   _waiters      = 0,
   _recursions   = 0;
   _object       = NULL;
   _owner        = NULL;
   _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet,只有獲取到鎖的線程才能wait
   _WaitSetLock  = 0 ;
   _Responsible  = NULL ;
   _succ         = NULL ;
   _cxq          = NULL ;
   FreeNext      = NULL ;
   _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
   _SpinFreq     = 0 ;
   _SpinClock    = 0 ;
   OwnerIsThread = 0 ;
 }

monitor中有兩個重要的隊列

  • waitSet:當持有monitor的線程調用wait方法時,當前線程會放棄monitor進入waitSet隊列等待,其他線程可以競爭獲取monitor,waitSet中的線程被喚醒後,不會立馬獲取到鎖,需要進入entrySet競爭獲取鎖
  • entrySet:上邊也提到了,被synchronized阻塞在臨界區外邊的線程將進入entrySet,獲取到鎖的線程釋放鎖後會喚醒entrySet中的線程

java字節碼分析

public class SyncCodeBlock {
    public int i;

    public void syncTask(){
        synchronized (this){
            i++;
        }
    }
}

字節碼:

public class com.fufu.concurrent.SyncCodeBlock {
  public int i;

  public com.fufu.concurrent.SyncCodeBlock();
    Code:
       0: aload_0
       1: invokespecial #1                  //構造方法
       4: return

  public void syncTask();
    Code:
       0: aload_0                           //鎖對象的引用
       1: dup                               //複製一份  
       2: astore_1                          //放到一個臨時變量slot_1
       3: monitorenter                   //注意此處,進入同步方法,將
       4: aload_0
       5: dup
       6: getfield      #2                  // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field i:I
      14: aload_1
      15: monitorenter                    //注意此處,退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit                    //注意此處,退出同步方法
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           4    16    19   any
          19    22    19   any
}

monitorenter做了什麼?

將lock對象和操作系統的monitor關聯,即將lock對象的markword指向關聯的monitor,owner改爲獲取到鎖的線程

monitorexit做了什麼?

將lock對象的markword重置,喚醒entrySet中等待的線程,owner置爲null

值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從字節碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。

如果synchronized加到方法上,則不是monitorenter和monitorexit,方法級的同步是隱式的同步,無需通過字節碼指令來控制,它實現在方法調用和返回中,當調用方法時,會先檢查方法訪問標誌中ACC_SYNCHRONIZED是否被設置,如果設置了執行線程會先持有monitor,方法返回時釋放monitor,如果一個同步方法執行期間拋出了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。

2.4 synchronized優化

2.4.1 輕量級鎖

輕量級鎖的使用場景:如果一個對象雖然有多線程要加鎖,但加鎖的時間是錯開的(也就是沒有競爭),那麼可以 使用輕量級鎖來優化。
輕量級鎖對使用者是透明的,即語法仍然是 synchronized

  • 每個線程都會包含一個鎖記錄的結構,內部可以存儲鎖定對象的markword,線程的鎖記錄結構中有鎖對象引用和鎖記錄地址,鎖狀態標識

在這裏插入圖片描述

加鎖過程:

  • 讓鎖記錄中的對象引用指向鎖對象,並嘗試CAS替換鎖對象中的markword

在這裏插入圖片描述

  • 如果cas替換成功,鎖對象的對象頭中就會存儲了線程鎖記錄結構中的鎖記錄地址和鎖狀態,表示該線程已經對該對象加鎖

在這裏插入圖片描述

  • 如果cas失敗,則分兩種情況:
    • 其他線程已經持有該對象鎖,加鎖失敗,存在競爭,則進入鎖膨脹
    • 該線程鎖重入,則count++,該線程在添加一條鎖記錄,鎖對象引用指向對象鎖,無需cas交換則原來用於交換的鎖記錄和鎖記錄地址爲null

在這裏插入圖片描述

解鎖過程:

  • 當退出 synchronized 代碼塊(解鎖時)如果有取值爲 null 的鎖記錄,表示有重入,這時重置鎖記錄,表示重 入計數減一
  • 當退出 synchronized 代碼塊(解鎖時)鎖記錄的值不爲 null,這時使用 cas 將 Mark Word 的值恢復給對象頭

在這裏插入圖片描述

總結

  • 加鎖過程:
    • 線程在棧楨中創建鎖記錄結構用於存儲鎖對象markword
    • 鎖記錄結構的對象引用指向鎖對象
    • cas替換鎖對象的markword和鎖記錄結構的鎖記錄地址
    • 此時鎖對象的對象頭中存儲了鎖記錄的地址,可以通過鎖記錄地址找到相應線程還有鎖狀態,輕量級鎖是00
    • 線程棧楨中存儲鎖對象的hashcode,分代年齡等信息
    • 如果cas交換失敗,如果是鎖重入現象的話,那麼在棧中在壓入一個棧楨,鎖記錄指向鎖對象,否則進入鎖膨脹
  • 解鎖過程:
    • 當退出 synchronized 代碼塊(解鎖時)如果有取值爲 null 的鎖記錄,表示有重入,這時重置鎖記錄,表示重 入計數減一
    • 當退出 synchronized 代碼塊(解鎖時)鎖記錄的值不爲 null,這時使用 cas 將 Mark Word 的值恢復給對象頭

2.4.2 重量級鎖

如果在嘗試加輕量級鎖的過程中,CAS 操作無法成功,這時一種情況就是有其它線程爲此對象加上了輕量級鎖(有 競爭),這時需要進行鎖膨脹,將輕量級鎖變爲重量級鎖

在這裏插入圖片描述

即對象頭中的鎖狀態和線程1中的鎖記錄結構中的鎖狀態相同

在這裏插入圖片描述

自旋優化

重量級鎖競爭的時候,還可以使用自旋來進行優化,如果當前線程自旋成功(即這時候持鎖線程已經退出了同步 塊,釋放了鎖),這時當前線程就可以避免阻塞

在 Java 6 之後自旋鎖是自適應的,比如對象剛剛的一次自旋操作成功過,那麼認爲這次自旋成功的可能性會 高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能。 Java 7 之後不能控制是否開啓自旋功能

2.4.3 偏向鎖

大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲取鎖的代價更低,引入了偏向鎖

只有第一次使用 CAS 將線程 ID 設置到對象的 Mark Word 頭,之後發現 這個線程 ID 是自己的就表示沒有競爭,不用重新 CAS。以後只要不發生競爭,這個對象就歸該線程所有

如果有其他線程競爭,則升級爲輕量級鎖

原先輕量級鎖每次鎖重入都需要cas替換markword

在這裏插入圖片描述

偏向鎖只有第一次加鎖時cas設置下線程id即可

在這裏插入圖片描述

偏向鎖撤銷

  • 調用對象的hashcode,偏向鎖時markword中存的是線程id
  • 其他線程使用偏向鎖,則升級爲輕量級鎖
  • 調用wait/notify,升級爲重量級鎖

三,volatile

  • 當寫一個volatile變量時,JMM會保證該線程對應的本地內存的共享變量值刷新回主內存
  • 當讀一個volatile變量時,JMM會使線程本地內存中原值失效,從主內存中讀取

volatile適合一個寫,多個讀的場景

3.1 可見性

保證了不同線程對一個共享變量操作時的可見性,即一個線程修改了共享變量的值會保證這個新值對其他線程可見

3.2 原子性

一個或多個操作,要麼全部執行並且執行過程中不會被中斷,要麼全部不執行,即使在多個線程一起執行時,一旦開始操作,就不會受其他線程影響

volatile實現了可見性和有序性,只能保證單條操作的原子性,不能保證複合操作的原子性

3.3 有序性

通過插入內存屏障禁止了處理器和編譯器某些類型的指令重排序,java編譯器在生成指令序列時,在適當的位置插入內存屏障

3.3.1 指令重排序

指令重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行重新排序的一種手段

指令重排的前提是指令重排後不能改變運行結果,存在數據依賴的指令不能重排

3.4 volatile原理

volatile的底層實現原理是內存屏障

  • 對volatile變量的寫指令後加入寫屏障
  • 對volatile變量的讀指令前加入讀屏障

3.4.1 可見性保證

寫屏障:保證在寫屏障之前對共享變量的改動都會同步到主內存中

    public void func(){
        //volatile變量前操作
        ready = true;
        //插入寫屏障:保證寫屏障前對共享變量的改動都同步到主內存中,沒有被volatile修飾的,在volatile變量前的共享變量都會被寫會到主存中
        //-----------------
        //volatile變量後操作
    }

讀屏障:保證在讀屏障之後對共享變量的讀取,加載的都是主內存中的最新數據

    public void func1(){
        //讀屏障
        //對volatile變量讀之前插入讀屏障,保證volatile讀取都是都是主內存中的最新數據
        if(ready){

        }else{

        }
    }

3.4.2 保證有序性

寫屏障:保證在寫屏障之前的操作不會被重排序到寫屏障之後

    public void func(){
        //volatile變量前操作
        ready = true;
        //插入寫屏障
        //-----------------
        //volatile變量後操作
    }

讀屏障:保證讀屏障之前的操作不會被重排序到讀屏障之後

    public void func1(){
        //讀屏障
        //對volatile變量讀之前插入讀屏障,保證volatile讀取都是都是主內存中的最新數據
        if(ready){

        }else{

        }
    }

有序性只能保證本線程內的代碼不被重排序

3.4.4 happens-before規則

happens-before規則是用來描述兩個操作的可見性的,如果A happens-before B 那麼A的結果對B可見

  • synchronized :線程解鎖m之前對共享變量的寫對接下來對m加鎖的其他線程對該變量的讀可見
  • volatile:對volatile變量的寫對volatile變量的讀可見
  • Thread:線程start()前的操作,對線程開始後的操作可見
  • Thread:線程結束前的操作對線程結束後的操作可見
  • Thread:T1中斷T2,對T2發現被中斷可見
  • Object:一個對象的構造函數的結束對這個對象的finalizer開始可見
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章