Java並發之Synchronized機制詳解


帶着問題閱讀

1、Synchronized如何使用,加鎖的粒度分別是什么

2、Synchronized的實現機制是什么

3、Synchronized是公平鎖嗎

4、Java對Synchronized做了哪些優化

Synchronized介紹

基本上所有的並發模式在解決線程沖突問題的時候,都是采用序列化訪問共享資源的方案。這意味着在給定時刻只允許一個任務訪問共享資源。通常這是通過在代碼前面加上一條鎖語句來實現的,這就使得在一段時間內只有一個任務可以運行這段代碼。因為鎖語句產生了一種互相排斥的效果,所以這種機制常常稱為互斥量(mutex)

為防止資源沖突,Java提供了Synchronized用於解決互斥訪問的問題。當任務執行被Synchronized修飾的代碼時,將先檢查鎖是否可用,然后獲取鎖、執行代碼,最后釋放鎖。考慮屋里有一個衛生間,多個人都需要單獨使用,為了使用衛生間,每個人都先敲門,看看能否使用,如果沒人使用他就進入衛生間並鎖上門,當其它人來的時候就會被擋在門外。

Synchronized使用方式

  • 對象鎖

synchronized可以用於修飾具體對象,如示例中分別對synObjthis對象加鎖,即synObjthis分別作為共享資源被用於互斥訪問,其中thread1thread2同時訪問synObj互斥,thread3thread4同時訪問this(demo對象)互斥。

public class Demo {
    private Object synObj = new Object();
    
    public void synObj() {
        // 對synObj對象加鎖
        synchronized(synObj) {
            // 同步代碼
        }
    }
    
    public void synThis() {
        // 對當前對象加鎖
        synchronized(this) {
            // 同步代碼
        }
    }
}

Demo demo = new Demo();
// 假設以下四個線程同時運行
new Thread(demo::synObj).start();   // thread1
new Thread(demo::synObj).start();   // thread2
new Thread(demo::synThis).start();  // thread3
new Thread(demo::synThis).start();  // thread4
  • 普通方法鎖

synchronized也可用於修飾方法,修飾方法時鎖的對象即this,因此如果類的多個方法上都添加了synchronized,那么這幾個方法在同步執行時也是互斥的。

public class Demo {
    public synchronized void test1() {};
    public synchronized void test2() {};
}

Demo demo = new Demo();
// 以下兩個線程同步執行時是互斥的,都需要獲取demo對象的鎖
new Thread(demo::test1).start();
new Thread(demo::test2).start();
  • 靜態方法鎖

以上兩種應用方式由於鎖的粒度都是對象,因此只能在並發調用同一個對象的方法是才會互斥,如果創建了Demo demo1 = new Demo()Demo demo2 = new Demo()兩個對象並分別調用,就不會產生互斥。如要在多實例之間也達成互斥,則可以通過修飾靜態方法來達成。

public class StaticDemo {
    private static Object obj = new Object();
    public void test() {
        synchronized(obj) {
            // 同步代碼
        }
    }
    
    public static synchronized void testStatic() {};
}

// 兩個線程互斥s
new Thread(StaticDemo::testStatic).start();
new Thread(StaticDemo::testStatic).start();
  • 類鎖

通過添加類鎖,也可實現多實例之間的互斥。synchronized修飾在靜態方法時,也等價於修飾當前類對象。

public class StaticDemo {
    public void test() {
        synchronized(StaticDemo.class) {
            // 同步代碼
        }
    }
}

Synchronized原理分析

不論synchronized用於修飾哪里,本質還是會修飾到具體的對象(實例對象或類對象)上,synchronized的實現機制也是對對象的加鎖。Java中每個對象都隱含關聯一個監視器ObjectMonitor,監視器通過cpp實現內置在JVM中,監視器地址記錄在對象的MarkWord上,synchronized通過ObjectMonitor實現對象的鎖操作。

對象頭MarkWord簡介

JVM在內存中將對象划為三部分:對象頭、實例數據和填充數據。對象頭分為MarkWord和類型指針兩部分,這里只針對鎖相關做進一步介紹。MarkWord用於存儲對象自身的運行數據,如哈希值、GC分代年齡等,這部分在32位和64位虛擬機中會分別占用32位和64位空間,以下是32位的空間布局(64位布局相同,分的bit數不同),MarkWord會根據對象狀態復用存儲空間,例如對象未鎖定狀態下,采用25bit哈希 + 4bitGC年齡 + 1bit固定0 + 2bit標志存儲。當標志位為10表示對象處於重量級鎖定時,剩余空間就用於存儲ObjectMonitor對象的地址。

MarkWord

ObjectMonitor簡介

ObjectMonitor() {
    ...
    _count = 0; // 記錄個數
    _owner = NULL;  // 記錄持有線程
    _cxq = NULL;    // 記錄鎖阻塞線程,與EntryList配合
    _WaitSet = NULL;    // 記錄處於wait狀態的線程
    _EntryList = NULL;  // 記錄處於鎖阻塞狀態的線程
    ...
}

ObjectMonitor整體內容略去,核心關注以上字段。_owner用於記錄持有線程,_count用於記錄重入次數,_cxq_EntryList配合用於記錄獲取鎖失敗阻塞后的線程。
線程獲取鎖失敗后會首先被掛載到_cxq隊列上並調用park阻塞。當鎖被釋放時,如_EntryList不為空,則嘗試喚醒_EntryList隊首元素;如_EntryList為空,默認從_cxq摘取隊首元素放入_EntryList並試圖獲取鎖。由於monitor鎖機制為非公平鎖,因此可能喚醒失敗,兩個隊列都會保存阻塞元素。

詳細解析可見參考第二篇文章

objectMonitor

Synchronized重量級鎖原理

public class Demo {
    private Object obj = new Object();
    public void test() {
        synchronized(obj) {
            System.out.println("lock");
        }
    }
}

編譯以上代碼,javap -v查看字節碼。

...
public void test();
    Code:
        ...
        monitorenter    // 加鎖
        ...
        monitorexit     // 釋放鎖
        ...
        return
...

其余內容略去,關鍵在於monitorentermonitorexit兩個指令。

當執行monitorenter時,將會嘗試獲取該對象monitor的所有權。

  • 如果monitor持有數為0即無線程持有,則直接獲取monitor並將進入數+1;
  • 如果monitor已被線程占有,檢查是否為當前線程,如是當前線程,則將計數器+1;否則阻塞當前線程。

當執行monitorexit時,將monitor計數器-1,如減后為0,則線程釋放monitor

synchronized修飾在方法上,則會在方法上增加ACC_SYNCHRONIZED的標記,原理與上述相同。

JVM對Synchronized的優化

monitorentermonitorexit依賴底層操作系統的mutex lock實現,該指令對線程的掛起和喚醒涉及到用戶態到內核態的切換,如果同步代碼頻繁調用,會帶來昂貴的切換開銷。自jdk1.6起對鎖的實現引入了大量優化,下面來介紹一下都做了哪些優化。

鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼要求同步,但是對被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判定一句來源於逃逸分析的數據支持,如果判斷一段代碼中,在堆上的所有數據都不會逃逸出去被其他線程訪問到,那就可以把它們當作棧上數據對待,認為是線程私有的,同步加鎖無須進行。

public String copyString(String s) {
    StringBuffer sb = new StringBuffer();
    sb.append(s);
    return sb.toString();
}

如示例代碼,StringBuffer.append是通過sychronized修飾的線程安全操作,但在該代碼塊中,sb對象是局部變量,僅會被當前線程訪問,不存在線程競爭,因此鎖經過編譯器檢測后可以消除。

鎖粗化

原則上編寫同步代碼時,推薦將同步塊的作用范圍限制的盡量小,一方面減少同步代碼塊的執行時間,一方面減少同步競爭次數,以便存在競爭時,等待鎖的線程可以盡快獲得鎖。但是如果一系列連續操作都在對同一對象反復加鎖和釋放鎖,那即使沒有線程競爭也會產生很多沒必要的開銷。

private Object obj = new Object();

public void lock() {
    synchronized(obj) {
        ...
    }
    // 再次加鎖
    synchronized(obj) {
        ...
    }
}

如上代碼,連續兩次對同一對象進行同步,即可將鎖粗化合並為一個鎖。

鎖消除和鎖粗化都是依賴JIT即時編譯實現,因此通過javac查看編譯后的字節碼,仍然保留着原始的鎖指令。

自旋鎖和自適應自旋

前文中我們提到,互斥同步涉及的掛起/喚醒線程都涉及內核態轉換,如果頻繁產生競爭會帶來很大的壓力。虛擬機開發團隊注意到很多應用對鎖的持有只會持續很短的時間,如果可以讓競爭鎖的線程稍等一下,不放棄處理器,就可以在持有鎖的線程執行完畢后獲取鎖,避免產生空間切換,這就是自旋

自旋鎖在jdk1.4.2中就引入,需要-XX: +UseSpining開啟,在jdk6以后就默認開啟了。自旋雖然避免了空間切換問題,但如果某個鎖競爭很激烈或者鎖的持有時間很長,那自旋只能白白占用處理器資源,因此在jdk1.6中引入了自適應自旋。自適應意味着自旋的時間不再固定,如果對一個鎖對象自旋等待剛剛成功過,則允許后續自旋等待較長時間;如果自旋很少成功,那就在后續獲得鎖的過程中直接跳過自旋。

偏向鎖

偏向鎖也是jdk1.6引入的優化,目的是消除數據在無競爭情況下的同步原語。鎖被第一個線程獲取后,在接下來的執行過程中,如果一直沒有被其他線程獲取,則持有偏向鎖的線程不在需要同步。

偏向鎖加鎖流程如下:

  • 檢查當前是否為偏向狀態。
  • 如果是,檢查當前線程ID與Mark Word記錄的線程ID是否一致,如一致則進入同步代碼,不一致則釋放偏向鎖
  • 如不是偏向鎖,則使用CAS嘗試修改線程ID,如修改成功則進入同步代碼,失敗則釋放偏向鎖

線程獲取偏向鎖后,持有鎖的線程以后每次進入相應同步塊時,都不需要再進行任何同步操作。

偏向鎖不會主動釋放,只有當其他線程嘗試獲取鎖時,才會檢查持有線程是否可以釋放鎖。如可以釋放則替換為新線程ID,不可釋放則升級為輕量級鎖。

勘誤:圖中如判斷對象頭Mark Word記錄非當前線程ID,下一步應當為開始偏向鎖撤銷而非CAS替換。如有不同意見歡迎留言

輕量級鎖

輕量級鎖在MarkWord標志位中由00表示,輕量級鎖首先在當前線程棧幀當中建立一個鎖記錄Lock Record,用於存儲MarkWord的拷貝;然后虛擬機使用CAS操作將Lock Record的地址記錄到MarkWord當中,並將標志位改為00,表示對象處於輕量級鎖定狀態。如果更新失敗,則會進入自旋並在自旋達到一定次數后升級為重量級鎖。自旋的同時如果有第三個線程嘗試獲取鎖,也會直接升級到重量級鎖。

同步代碼執行完畢后,輕量級鎖同樣使用CAS操作將棧幀中的MarkWord拷貝回到對象中,如果操作成功,則釋放鎖;如果替換失敗,則說明有其他線程在競爭鎖(意味着升級為重量級鎖),則當前膨脹為重量鎖轉換為重量鎖的釋放。

重量級鎖

重量級鎖即上文Synchronized重量級鎖原理所述內容,綜上synchronized的加鎖過程為偏向鎖 -> 輕量級鎖 -> 重量級鎖,這個過程也稱為鎖膨脹

圖源自網絡

總結

最后總結對比一下幾種鎖實現。

鎖類型 運行空間 實現機制 適用范圍
偏向鎖 用戶態 初次CAS加鎖,后續如無競爭可直接進入 單線程執行
輕量級鎖 用戶態 CAS+自旋加鎖 鎖競爭不激烈
重量級鎖 內核態 mutex 內核態操作 鎖激烈競爭

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM