天天看點

一文帶你讀懂JDK源碼:synchronized

Java提供的常用同步手段之一就是sychronized關鍵字,synchronized 是利用鎖的機制來實作同步的。

下文我們從3個角度深入剖析synchronized的應用原理:

  • synchronized的四個特點(原子性/有序性/互斥性/可重入)
  • synchronized的兩種鎖分類(類鎖/對象鎖)
  • synchronized與ReentrantLock的差別

winter

必須先提及一個重要的基礎概念:Monitor監聽機制。Monitor是什麼?Monitor 是Java中實作 synchronized關鍵字的基礎,可以将它了解為一個監聽器,是用來實作同步的工具,monitor與每一個Java對象與class位元組碼相關聯。monitor對象存在于每個Java對象的對象頭中,synchronized鎖便是通過這種方式擷取鎖的,也是為什麼Java中任意對象可以作為鎖的原因。(參考:《JVM鎖優化》)Monitor的本質?

Monitor 在JVM中是基于C++的實作的,ObjectMonitor中有幾個關鍵屬性,見下圖:

一文帶你讀懂JDK源碼:synchronized
  • _owner:指向持有ObjectMonitor對象的線程

  • _WaitSet:存放處于wait狀态的線程隊列

  • _EntryList:存放處于等待鎖block狀态的線程隊列

  • _recursions:鎖的重入次數

  • _count:用來記錄該線程擷取鎖的次數

加鎖過程:當多個線程(A/B/C)同時通路一段同步代碼時,首先會進入_EntryList隊列中,當某個線程A擷取到對象的monitor後進入_Owner區域并把monitor中的_owner變量設定為目前線程,同時monitor中的計數器_count加1,即(該線程A)獲得鎖。線程B/C都進入了_EntryList裡面,線程A進入了_Owner。

    

釋放鎖過程:

若持有monitor的線程A調用wait()方法,将釋放它目前持有的monitor,_owner變量恢複為null,_count自減1,同時線程A進入_WaitSet集合中等待被喚醒。

此時在_EntryList的線程B/C會競争擷取monitor,假設結果是B線程競争成功并進入了_Owner。線程C留在了_EntryList裡面,線程A進入了_WaitSet。

若目前線程B執行完畢也将釋放monitor(鎖)并複位變量的值,以便其他線程進入擷取monitor(鎖)。線程B可以通過notify/notifyAll 來喚醒 _WaitSet 的線程A,此時_WaitSet 的線程A 與 _EntryList 的線程C會同時進行鎖資源競争。注意:1、由于notify喚醒線程具有随機性,甚至導緻死鎖發生;是以一般建議使用notifyAll。2、不管喚醒一個線程,還是喚醒多個線程,最終獲得對象鎖的,隻有一個線程。如果_EntryList同時存在競争鎖資源的線程,那麼被喚醒的線程還需要和_EntryList中的線程一起競争鎖資源。但是JVM保證最終隻會讓一個線程擷取到鎖。

一文帶你讀懂JDK源碼:synchronized
一文帶你讀懂JDK源碼:synchronized
一文帶你讀懂JDK源碼:synchronized

synchronized 的四個特征

基于 monitor 機制,引出了 synchronized 的四個特征:

1.原子性

基于monitor螢幕,被synchronized修飾的類或對象的所有操作都是原子的,因為在執行操作之前必須先獲得類或對象的鎖,直到執行完才能釋放,這中間的過程無法被中斷(除了已經廢棄的stop()方法),即保證了原子性。

2.可見性

基于monitor螢幕,synchronized 對一個類或對象加鎖時,一個線程如果要通路該類或對象必須先獲得它的鎖,而這個鎖的狀态對于其他任何線程都是可見的。在釋放鎖之前會将對變量的修改重新整理到主記憶體當中,進而保證資源變量的可見性。

3.有序性

基于monitor螢幕,有效解決重排序問題:指令重排并不會影響單線程的順序和結果,它影響的是多線程并發執行的順序性。而 synchronized 保證了每個時刻都隻有一個線程通路同步代碼塊,也就确定了線程執行同步代碼塊是分先後順序的,保證了有序性。

4.可重入性

synchronized和ReentrantLock都是可重入鎖。

當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,将會處于阻塞狀态;

當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖。通俗一點講就是說一個線程擁有了鎖仍然還可以重複申請鎖。

synchronized 的效果:可以具體展現為 “monitorenter”與“monitorexit”兩條指令(一個monitor exit指令之前都必須有一個monitor enter),下面是編譯檔案的例子:

一文帶你讀懂JDK源碼:synchronized

對 synchronized 的優化,參考《JVM的鎖優化》可知,鎖更新的過程是:偏向鎖 -> 輕量級鎖 -> 重量級鎖。

一文帶你讀懂JDK源碼:synchronized
一文帶你讀懂JDK源碼:synchronized
一文帶你讀懂JDK源碼:synchronized

synchronized支援類鎖與對象鎖

例子1:類鎖對于類鎖,我們必須了解兩種使用場景:

  • 修飾一個靜态的方法:其作用的範圍是整個方法,作用的對象是這個類的所有對象
  • 修飾一個類:其作用的範圍是synchronized後面括号括起來的部分,作用的對象是這個類的所有對象

例子1.1:修飾一個靜态的方法

//類鎖:靜态方法 - 修飾一個靜态的方法              public static synchronized void lock() throws InterruptedException {              //延時1s執行日志輸出              TimeUnit.SECONDS.sleep(1);              System.out.println("lock1 executeTime = " + System.currentTimeMillis());              }           

例子1.2:修飾一個類

//類鎖:類名 - 修飾一個類              public static void lock2() throws InterruptedException {              synchronized (ClassLock.class){              //延時1s執行日志輸出              TimeUnit.SECONDS.sleep(1);              System.out.println("lock2 executeTime = " + System.currentTimeMillis());              }              }           

測試用例:

/**              * <p>              *     類鎖資源競争例子:              *      1、修飾一個靜态的方法              *      2、修飾一個類              * </p>              */              public class ClassLock {              public static void main(String[] args) throws InterruptedException{              Thread t1 = new Thread(new Runnable() {              @Override              public void run() {              ClassLock classLock = new ClassLock();              try {              //          classLock.lock();              classLock.lock2();              } catch (InterruptedException e) {              e.printStackTrace();              }              }              });              Thread t2 = new Thread(new Runnable() {              @Override              public void run() {              ClassLock classLock = new ClassLock();              try {              //          classLock.lock();              classLock.lock2();              } catch (InterruptedException e) {              e.printStackTrace();              }              }              });              t1.start();              t2.start();              //由于t1 和 t2 存在類鎖資源競争,是以兩個線程真正執行時間是不一樣的                }              }           

輸出結果:類鎖的兩種鎖都存在競争互斥,是以代碼段都不是同時被執行。

lock1 executeTime = 1616499560230              lock2 executeTime = 1616499561230              lock1 executeTime = 1616499562231              lock2 executeTime = 1616499563231           

例子2:對象鎖對于對象鎖,我們必須了解兩種使用場景:

  • 修飾一個方法:被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的對象是調用這個方法的對象;
  • 修飾一個代碼塊:被修飾的代碼塊稱為同步語句塊,其作用範圍是大括号{}括起來的代碼塊,作用的對象是調用這個代碼塊的對象;

例子2.1:修飾一個方法

//對象鎖:普通方法              public synchronized void lock() throws InterruptedException {              //延時1s執行日志輸出              TimeUnit.SECONDS.sleep(1);              System.out.println("lock1 executeTime = " + System.currentTimeMillis());              }           

例子2.2:修飾一個代碼塊

//對象鎖:普通方法代碼塊              public void lock2() throws InterruptedException {              synchronized (this){              //延時1s執行日志輸出              TimeUnit.SECONDS.sleep(1);              System.out.println("lock2 executeTime = " + System.currentTimeMillis());              }              }           
public class ObjectLock {              public static void main(String[] args) throws InterruptedException{              Thread t1 = new Thread(new Runnable() {              @Override              public void run() {              ObjectLock classLock = new ObjectLock();              try {              classLock.lock();              //          classLock.lock2();              } catch (InterruptedException e) {              e.printStackTrace();              }              }              });              Thread t2 = new Thread(new Runnable() {              @Override              public void run() {              ObjectLock classLock = new ObjectLock();              try {              classLock.lock();              //          classLock.lock2();              } catch (InterruptedException e) {              e.printStackTrace();              }              }              });              t1.start();              t2.start();              //由于t1 和 t2 不存在對象鎖資源競争,是以兩個線程真正執行時間一樣              }              }           

輸出結果:多線程使用的對象鎖不存在互斥競争,是以都是同時被執行了。

lock1 executeTime = 1616499668823              lock1 executeTime = 1616499668823              lock2 executeTime = 1616499669823              lock2 executeTime = 1616499669823           
一文帶你讀懂JDK源碼:synchronized
一文帶你讀懂JDK源碼:synchronized

與ReentrantLock的差別

下面分别從六個角度闡述兩者(synchronized 與 ReentrantLock)的差別:

底層實作/可中斷機制支援/釋放鎖方式(手動/非手動)/鎖類型(公平鎖/非公平鎖)/等待線程的精确喚醒/鎖對象。

1、底層實作

synchronized 是JVM層面的鎖,是Java關鍵字,通過monitor對象來完成(monitorenter與monitorexit),對象隻有在同步塊或同步方法中才能調用wait/notify方法;同時涉及到鎖的更新,具體為無鎖、偏向鎖、自旋鎖、向OS申請重量級鎖(參考另一篇文章:JVM鎖的更新)ReentrantLock 是從jdk1.5以來(java.util.concurrent.locks.Lock)提供的API層面的鎖,是通過利用CAS(CompareAndSwap)自旋機制保證線程操作的原子性和volatile保證資料可見性以實作鎖的功能。

    2、不可中斷執行synchronized是不可中斷類型的鎖,除非加鎖的代碼中出現異常或正常執行完成;

ReentrantLock則可以中斷,可通過trylock(long timeout,TimeUnit unit)設定逾時方法或者将lockInterruptibly()放到代碼塊中,調用interrupt方法進行中斷。

    3、jvm底層釋放資源

synchronized 不需要使用者去手動釋放鎖,synchronized 代碼執行完後系統會自動讓線程釋放對鎖的占用;ReentrantLock則需要使用者去手動釋放鎖,如果沒有手動釋放鎖,就可能導緻死鎖現象。一般通過lock()和unlock()方法配合try/finally語句塊來完成,使用釋放更加靈活。

    4、是否公平鎖 synchronized為非公平鎖(參考開頭的Monitor鎖資源競争政策);ReentrantLock則即可以選公平鎖也可以選非公平鎖,通過構造方法new ReentrantLock時傳入boolean值進行選擇,為空預設false非公平鎖,true為公平鎖。

    5、鎖是否可綁定條件Condition進行準确的線程喚醒synchronized不能綁定并精确喚醒某一個線程資源,通過Object類的wait()/notify()/notifyAll()方法要麼随機喚醒一個線程要麼喚醒全部線程;ReentrantLock通過綁定Condition結合await()/singal()方法實作線程的精确喚醒,而不是像synchronized。

    6、鎖的對象synchronzied鎖的是對象,鎖是儲存在對象頭裡面的,根據對象頭資料來辨別是否有線程獲得鎖/争搶鎖;ReentrantLock鎖的是線程,根據進入的線程,和int類型的state辨別鎖的獲得/争搶。

一文帶你讀懂JDK源碼:synchronized
一文帶你讀懂JDK源碼:synchronized

總結

上文結合synchronized的底層原理 -- Monitor機制,分别從3個角度(synchronized的特點/鎖分類/與ReentrantLock的差別)剖析了 synchronized 的原理與應用。

後續我們會繼續探讨 volatile 重排序 與 ReentranLock 源碼和底層原理,希望對大家有所幫助。

掃描二維碼

擷取技術幹貨

一文帶你讀懂JDK源碼:synchronized

繼續閱讀