多線程筆記--鎖(synchronized)

  • synchronized

在併發編程中存在線程安全問題,主要緣由有:1.存在共享數據 2.多線程共同操做共享數據。關鍵字synchronized能夠保證在同一時刻,只有一個線程能夠執行某個方法或某個代碼塊,同時synchronized能夠保證一個線程的變化可見(可見性),便可以代替volatile。java

實現原理和做用

synchronized能夠保證方法或者代碼塊在運行時,同一時刻只有一個方法能夠進入到臨界區,同時它還能夠保證共享變量的內存可見性,它能夠:git

  • 原子性:確保線程互斥的訪問同步代碼
  • 可見性:保證共享變量的修改可以及時可見
  • 有序性:有效解決重排序問題。即「一個unlock操做先行發生(happen-before)於後面對同一個鎖的lock操做」;

鎖的三種應用方式

Java中每個對象均可以做爲鎖,這是synchronized實現同步的基礎:github

  1. 普通同步方法(實例方法),鎖是當前實例對象 ,進入同步代碼前要得到當前實例的鎖
  2. 靜態同步方法,鎖是當前類的class對象 ,進入同步代碼前要得到當前類對象的鎖
  3. 同步方法塊,鎖是括號裏面的對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。 [圖片上傳失敗...(image-bac42-1557385226672)] 如圖,synchronized能夠用在方法上也可使用在代碼塊中,其中方法是實例方法和靜態方法分別鎖的是該類的實例對象和該類的對象。而使用在代碼塊中也能夠分爲三種,具體的能夠看上面的表格。這裏的須要注意的是:若是鎖的是類對象的話,儘管new多個實例對象,但他們仍然是屬於同一個類依然會被鎖住,即線程之間保證同步關係。
  • 對象頭

在同步的時候是獲取對象的monitor,即獲取到對象的鎖。那麼對象的鎖怎麼理解?無非就是相似對對象的一個標誌,那麼這個標誌就是存放在Java對象的對象頭。Java對象頭裏的Mark Word裏默認的存放的對象的Hashcode,分代年齡和鎖標記位。編程

每一個對象分爲三塊區域:對象頭、實例數據和對齊填充數組

  • 對象頭包含兩部分,第一部分是Mark Word,用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等等,這一部分佔一個字節。第二部分是Klass Pointer(類型指針),是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例,這部分也佔一個字節。(若是對象是數組類型的,則須要3個字節來存儲對象頭,由於還須要一個字節存儲數組的長度)
  • 實例數據存放的是類屬性數據信息,包括父類的屬性信息,若是是數組的實例部分還包括數組的長度,這部份內存按4字節對齊。
  • 填充數據是由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊
鎖狀態 25bit 4bit 1bit是不是偏向鎖 2bit鎖標記位
無鎖 對象的haahcode 分代年齡 0 01
輕量級鎖 指向棧中鎖記錄的指針 合併第一列 合併第一列 00
重量級鎖 指向互斥量(重量級鎖)的指針 合併第一列 合併第一列 10
GC標誌 合併第一列 合併第一列 11
偏向鎖 線程ID(23bit)和Epoch(2bit) 對象分代年齡 1 01

如上表在Mark Word會默認存放hasdcode,年齡值以及鎖標誌位等信息安全

鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率。bash

  • 對象鎖(monitor)機制,也叫監視器

從語法上講,Synchronized能夠把任何一個非null對象做爲"鎖",在HotSpot JVM實現中,鎖有個專門的名字:對象監視器(Object Monitor)。能夠把它理解爲 一個同步工具,也能夠描述爲 一種同步機制,實現了在一個時間點,最多隻有一個線程在執行管程的某個子程序,這個機制的保障來源於監視鎖Monitor,每一個對象都擁有本身的監視鎖Monitor。數據結構

咱們能夠把監視器理解爲一個醫院,醫院裏面只要一個醫生,每次只能看一個病人(線程),若是一個病人想看病,他首先要在走廊裏面排隊(Entry Set),依次進入看病,可是假如某個正在看病的人可能暈血或者血糖低不能暫時繼續看病(線程被掛起),這時候不能強行給他看,也不能讓後面的病人等他一個,因而就要送他到休息室去休息(Wait Set),休息室裏面呆的都是由於各類緣由不能繼續看病的病人,等休息好了,還能夠繼續去看病。以下圖多線程

靈魂畫做
總之,監視器是一個用來監視這些線程進入特殊的房間的。他的義務是保證(同一時間)只有一個線程能夠訪問被保護的數據和代碼。

Monitor的實現原理

在Java虛擬機(HotSpot)中,Monitor是基於C++實現的ObjectMonitor,其主要數據結構以下併發

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //用來記錄該線程獲取鎖的次數
    _waiters      = 0,
    _recursions   = 0; //鎖的重入次數
    _object       = NULL;
    _owner        = NULL;  //指向當前持有ObjectMonitor對象的線程
    _WaitSet      = NULL;  //存放wait狀態的線程隊列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //這是一個和_WaitSet相似存等待線程的地方,
                           //可是是否存在這裏是要根據Policy的值(這裏不知道說的對不對,順便說下,這玩意兒每次看都覺得是cxk)      
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放處於等待鎖的線程隊列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
複製代碼

當多個線程同時訪問一段同步代碼時,首先進入 _EntryList,當某個線程獲取到對象的monitor後進入_Owner區域並把monitor中的_owner變量設置爲當前線程,同時_count加一,得到對象鎖。 若是持有monitor的線程被掛起(例如調用wait方法),將釋放當前持有的monitor,_owner變量回復爲null,_count減一,同時該線程進入_WaitSet隊列中等待被喚醒(notify),若是當前線程順利執行完代碼塊後會釋放monitor並復位變量的值,以便下一個線程進來獲取monitor鎖,下面看個例子。

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
            System.out.printf("synchronized");
        }
        function();
    }

    private static void function() {
        System.out.printf("function");
    }
}
上面的代碼中有一個同步代碼塊,鎖住的是類對象,而且還有一個同步靜態方法,鎖住的依然是該類的類對象。下面是字節碼文件
public class com.example.javalib.SynchronizedDemo {
  public com.example.javalib.SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2 // class com/example/javalib/SynchronizedDemo
       2: dup
       3: astore_1
       4: monitorenter
       5: aload_1
       6: monitorexit
       7: goto          15
      10: astore_2
      11: aload_1
      12: monitorexit
      13: aload_2
      14: athrow
      15: invokestatic  #3 // Method function:()V
      18: return
複製代碼

上面的4,6,12行就是須要注意的部分了,這是添加Synchronized關鍵字以後纔會出現的。執行同步代碼塊首先要執行monitorenter,退出的時候執行monitorexit指令。 使用Synchronized之因此可以進行同步,其關鍵就是對對象的監視器monitor的獲取,當執行線程獲取到monitor後才能繼續執行下去,不然只能繼續等待。 上面的demo中同步代碼塊後還有一個靜態方法,這個方法是同步的,並且該方法鎖的對象依然是這個類對象,那麼執行線程就沒必要再去獲取這個鎖,從字節碼中能夠看到,有一條monitorenter指令和兩條monitorexit指令,並無第二次獲取鎖的指令,這就是鎖的重入性:即在同一個鎖程中,線程不須要去再次獲取同一把鎖,Synchronized先天具備重入性。每一個對象擁有一個計數器,當線程獲取該對象鎖後,計數器就會加一,釋放鎖後就會將計數器減一。 任意一個對象都擁有本身的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取該對象的監視器才能進入同步塊和同步方法,若是沒有獲取到監視器的線程將會被阻塞在同步塊和同步方法的入口處,進入到BLOCKED狀態,關於線程的狀態能夠看這篇文章

從上面咱們知道了sychronized加鎖的時候,會調用objectMonitorenter方法,解鎖的時候會調用exit方法。事實上,只有在JDK1.6以前,synchronized的實現纔會直接調用ObjectMonitorenterexit,這種鎖被稱之爲重量級鎖。爲何說這種方式操做鎖很重呢? 由於Java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統的幫忙,這就要從用戶態轉換到核心態,所以狀態轉換須要花費不少的處理器時間,對於代碼簡單的同步塊(如被synchronized修飾的get 或set方法)狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長,因此說synchronized是java語言中一個重量級的操縱。 因此,在JDK1.6中出現對鎖進行了不少的優化,進而出現輕量級鎖,偏向鎖,鎖消除,適應性自旋鎖,鎖粗化(自旋鎖在1.4就有 只不過默認的是關閉的,jdk1.6是默認開啓的),這些操做都是爲了在線程之間更高效的共享數據 ,解決競爭問題。

感謝參考文章

以上文章是解決一個同步問題時發現synchronized知識點只知其一;不知其二後查找資料後摘抄的筆記,算是本身我的的整理,漏了什麼歡迎指出來。

完全理解synchronized 深刻多線程系列 深刻分析Synchronized原理

相關文章
相關標籤/搜索