synchronized原理

Java并發(fā)編程中synchronized是元老級(jí)角色,因?yàn)槭褂煤?jiǎn)單,所以是Java程序員使用最多的同步策略。在Java 6之前相比JUC的Lock是一個(gè)重量級(jí)的鎖,但是隨著Java6對(duì)synchronized的各種優(yōu)化,synchronized已經(jīng)輕量了很多,因此synchronized依然是Java專家組首推的同步策略。

本文主要介紹synchronized的實(shí)現(xiàn)原理,接下來的一篇文章將介紹Java 6中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級(jí)鎖,以及鎖的存儲(chǔ)結(jié)構(gòu)和升級(jí)過程。

一、基本概念

1、Java中每一個(gè)對(duì)象都包含一個(gè)監(jiān)視器,所以每一個(gè)對(duì)象都可以作為鎖

2、當(dāng)一個(gè)線程試圖訪問同步代碼塊時(shí),它首先必須得到鎖,退出或拋出異常時(shí)必須釋放鎖

3、synchronized不同用法鎖對(duì)象說明

修飾在靜態(tài)方法上,鎖對(duì)象是當(dāng)前類的Class對(duì)象

修飾在實(shí)例方法上,鎖對(duì)象是當(dāng)前實(shí)例對(duì)象

同步塊中,鎖對(duì)象是synchronized括號(hào)后面的對(duì)象

//如下demo的4個(gè)方法展示了不同使用方法下鎖對(duì)象
public class SynchronizedDemo {

    private static final Object LOCK = new Object();

    public static synchronized void s1(){
        System.out.println("類同步方法,鎖對(duì)象是當(dāng)前Class對(duì)象");
    }

    public synchronized void s2() {
        System.out.println("實(shí)例同步方法,鎖對(duì)象是當(dāng)前對(duì)象");
    }

    public void s3() {
        synchronized (LOCK) {
            System.out.println("同步塊,鎖對(duì)象是LOCK對(duì)象");
        }
    }

    public void s4() {
        synchronized (SynchronizedDemo.class) {
            System.out.println("同步塊,鎖對(duì)象和靜態(tài)同步方法的鎖對(duì)象一樣都是當(dāng)前Class對(duì)象");
        }
    }

}

實(shí)現(xiàn)原理

JVM規(guī)范規(guī)定JVM基于進(jìn)入和退出Monitor對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣:

1、代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn),monitorexit是插入到方法結(jié)束處和異常處, JVM要保證每個(gè)monitorenter必須有對(duì)應(yīng)的monitorexit與之配對(duì)。

2、方法同步則是方法修飾符上有個(gè)ACC_SYNCHRONIZED。

兩種實(shí)現(xiàn)的本質(zhì)是一樣的,任何對(duì)象都有一個(gè) monitor 與之關(guān)聯(lián),當(dāng)且一個(gè)monitor 被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter 指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的 monitor 的所有權(quán),即嘗試獲得對(duì)象的鎖。

javap查看字節(jié)碼文件

通過javap -v SynchronizedDemo .class 查看反編譯代碼

同步方法的修飾符有個(gè)ACC_SYNCHRONIZED標(biāo)記

image

同步塊使用monitorenter和monitorexit兩個(gè)指令管理同步塊

image

如下是從網(wǎng)上找到的JVM關(guān)于monitorenter和moniterexit的解釋

monitorenter:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
? If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
? If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
? If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

這段話的大概意思為:

每個(gè)對(duì)象有一個(gè)監(jiān)視器鎖(monitor)。當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán),過程如下:

1、如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者。

2、如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1.

3.如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。

monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

這段話的大概意思為:

執(zhí)行monitorexit的線程必須是objectref所對(duì)應(yīng)的monitor的所有者。

指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè) monitor 的所有權(quán)。

通過這兩段描述,我們應(yīng)該能很清楚的看出Synchronized的實(shí)現(xiàn)原理,Synchronized的語義底層是通過一個(gè)monitor的對(duì)象來完成,其實(shí)wait/notify等方法也依賴于monitor對(duì)象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會(huì)拋出java.lang.IllegalMonitorStateException的異常的原因。

偏向鎖

引入偏向鎖的目的:在沒有多線程競(jìng)爭(zhēng)的情況下,盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑,輕量級(jí)鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只依賴一次CAS原子指令置換ThreadID,不過一旦出現(xiàn)多個(gè)線程競(jìng)爭(zhēng)時(shí)必須撤銷偏向鎖,所以撤銷偏向鎖消耗的性能必須小于之前節(jié)省下來的CAS原子操作的性能消耗,不然就得不償失了。JDK 1.6中默認(rèn)開啟偏向鎖,可以通過-XX:-UseBiasedLocking來禁用偏向鎖。

在HotSpot中,偏向鎖的入口位于synchronizer.cpp文件的ObjectSynchronizer::fast_enter函數(shù):

image
偏向鎖的獲取

偏向鎖的獲取由BiasedLocking::revoke_and_rebias方法實(shí)現(xiàn),由于實(shí)現(xiàn)比較長(zhǎng),就不貼代碼了,實(shí)現(xiàn)邏輯如下:
1、通過markOop mark = obj->mark()獲取對(duì)象的markOop數(shù)據(jù)mark,即對(duì)象頭的Mark Word;
2、判斷mark是否為可偏向狀態(tài),即mark的偏向鎖標(biāo)志位為 1,鎖標(biāo)志位為 01;
3、判斷mark中JavaThread的狀態(tài):如果為空,則進(jìn)入步驟(4);如果指向當(dāng)前線程,則執(zhí)行同步代碼塊;如果指向其它線程,進(jìn)入步驟(5);
4、通過CAS原子指令設(shè)置mark中JavaThread為當(dāng)前線程ID,如果執(zhí)行CAS成功,則執(zhí)行同步代碼塊,否則進(jìn)入步驟(5);
5、如果執(zhí)行CAS失敗,表示當(dāng)前存在多個(gè)線程競(jìng)爭(zhēng)鎖,當(dāng)達(dá)到全局安全點(diǎn)(safepoint),獲得偏向鎖的線程被掛起,撤銷偏向鎖,并升級(jí)為輕量級(jí),升級(jí)完成后被阻塞在安全點(diǎn)的線程繼續(xù)執(zhí)行同步代碼塊;

偏向鎖的撤銷

只有當(dāng)其它線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,偏向鎖的撤銷由BiasedLocking::revoke_at_safepoint方法實(shí)現(xiàn):

image

1、偏向鎖的撤銷動(dòng)作必須等待全局安全點(diǎn);
2、暫停擁有偏向鎖的線程,判斷鎖對(duì)象是否處于被鎖定狀態(tài);
3、撤銷偏向鎖,恢復(fù)到無鎖(標(biāo)志位為 01)或輕量級(jí)鎖(標(biāo)志位為 00)的狀態(tài);

偏向鎖在Java 1.6之后是默認(rèn)啟用的,但在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,可以使用-XX:BiasedLockingStartupDelay=0參數(shù)關(guān)閉延遲,如果確定應(yīng)用程序中所有鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過XX:-UseBiasedLocking=false參數(shù)關(guān)閉偏向鎖。

輕量級(jí)鎖

引入輕量級(jí)鎖的目的:在多線程交替執(zhí)行同步塊的情況下,盡量避免重量級(jí)鎖引起的性能消耗,但是如果多個(gè)線程在同一時(shí)刻進(jìn)入臨界區(qū),會(huì)導(dǎo)致輕量級(jí)鎖膨脹升級(jí)重量級(jí)鎖,所以輕量級(jí)鎖的出現(xiàn)并非是要替代重量級(jí)鎖。

輕量級(jí)鎖的獲取

當(dāng)關(guān)閉偏向鎖功能,或多個(gè)線程競(jìng)爭(zhēng)偏向鎖導(dǎo)致偏向鎖升級(jí)為輕量級(jí)鎖,會(huì)嘗試獲取輕量級(jí)鎖,其入口位于ObjectSynchronizer::slow_enter

image

1、markOop mark = obj->mark()方法獲取對(duì)象的markOop數(shù)據(jù)mark;
2、mark->is_neutral()方法判斷mark是否為無鎖狀態(tài):mark的偏向鎖標(biāo)志位為 0,鎖標(biāo)志位為 01
3、如果mark處于無鎖狀態(tài),則進(jìn)入步驟(4),否則執(zhí)行步驟(6);
4、把mark保存到BasicLock對(duì)象的_displaced_header字段;
5、通過CAS嘗試將Mark Word更新為指向BasicLock對(duì)象的指針,如果更新成功,表示競(jìng)爭(zhēng)到鎖,則執(zhí)行同步代碼,否則執(zhí)行步驟(6);
6、如果當(dāng)前mark處于加鎖狀態(tài),且mark中的ptr指針指向當(dāng)前線程的棧幀,則執(zhí)行同步代碼,否則說明有多個(gè)線程競(jìng)爭(zhēng)輕量級(jí)鎖,輕量級(jí)鎖需要膨脹升級(jí)為重量級(jí)鎖;

假設(shè)線程A和B同時(shí)執(zhí)行到臨界區(qū)if (mark->is_neutral())
1、線程AB都把Mark Word復(fù)制到各自的_displaced_header字段,該數(shù)據(jù)保存在線程的棧幀上,是線程私有的;
2、Atomic::cmpxchg_ptr原子操作保證只有一個(gè)線程可以把指向棧幀的指針復(fù)制到Mark Word,假設(shè)此時(shí)線程A執(zhí)行成功,并返回繼續(xù)執(zhí)行同步代碼塊;
3、線程B執(zhí)行失敗,退出臨界區(qū),通過ObjectSynchronizer::inflate方法開始膨脹鎖;

輕量級(jí)鎖的釋放

輕量級(jí)鎖的釋放通過ObjectSynchronizer::fast_exit完成。

image

1、確保處于偏向鎖狀態(tài)時(shí)不會(huì)執(zhí)行這段邏輯;
2、取出在獲取輕量級(jí)鎖時(shí)保存在BasicLock對(duì)象的mark數(shù)據(jù)dhw;
3、通過CAS嘗試把dhw替換到當(dāng)前的Mark Word,如果CAS成功,說明成功的釋放了鎖,否則執(zhí)行步驟(4);
4、如果CAS失敗,說明有其它線程在嘗試獲取該鎖,這時(shí)需要將該鎖升級(jí)為重量級(jí)鎖,并釋放;

重量級(jí)鎖

重量級(jí)鎖通過對(duì)象內(nèi)部的監(jiān)視器(monitor)實(shí)現(xiàn),其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn),操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。

鎖膨脹過程

鎖的膨脹過程通過ObjectSynchronizer::inflate函數(shù)實(shí)現(xiàn)

image

膨脹過程的實(shí)現(xiàn)比較復(fù)雜,截圖中只是一小部分邏輯,完整的方法可以查看synchronized.cpp,大概實(shí)現(xiàn)過程如下:
1、整個(gè)膨脹過程在自旋下完成;
2、mark->has_monitor()方法判斷當(dāng)前是否為重量級(jí)鎖,即Mark Word的鎖標(biāo)識(shí)位為 10,如果當(dāng)前狀態(tài)為重量級(jí)鎖,執(zhí)行步驟(3),否則執(zhí)行步驟(4);
3、mark->monitor()方法獲取指向ObjectMonitor的指針,并返回,說明膨脹過程已經(jīng)完成;
4、如果當(dāng)前鎖處于膨脹中,說明該鎖正在被其它線程執(zhí)行膨脹操作,則當(dāng)前線程就進(jìn)行自旋等待鎖膨脹完成,這里需要注意一點(diǎn),雖然是自旋操作,但不會(huì)一直占用cpu資源,每隔一段時(shí)間會(huì)通過os::NakedYield方法放棄cpu資源,或通過park方法掛起;如果其他線程完成鎖的膨脹操作,則退出自旋并返回;
5、如果當(dāng)前是輕量級(jí)鎖狀態(tài),即鎖標(biāo)識(shí)位為 00,膨脹過程如下:

image

1、通過omAlloc方法,獲取一個(gè)可用的ObjectMonitor monitor,并重置monitor數(shù)據(jù);
2、通過CAS嘗試將Mark Word設(shè)置為markOopDesc:INFLATING,標(biāo)識(shí)當(dāng)前鎖正在膨脹中,如果CAS失敗,說明同一時(shí)刻其它線程已經(jīng)將Mark Word設(shè)置為markOopDesc:INFLATING,當(dāng)前線程進(jìn)行自旋等待膨脹完成;
3、如果CAS成功,設(shè)置monitor的各個(gè)字段:_header、_owner和_object等,并返回;

monitor競(jìng)爭(zhēng)

當(dāng)鎖膨脹完成并返回對(duì)應(yīng)的monitor時(shí),并不表示該線程競(jìng)爭(zhēng)到了鎖,真正的鎖競(jìng)爭(zhēng)發(fā)生在ObjectMonitor::enter方法中。

image

1、通過CAS嘗試把monitor的_owner字段設(shè)置為當(dāng)前線程;
2、如果設(shè)置之前的_owner指向當(dāng)前線程,說明當(dāng)前線程再次進(jìn)入monitor,即重入鎖,執(zhí)行_recursions ++ ,記錄重入的次數(shù);
3、如果之前的_owner指向的地址在當(dāng)前線程中,這種描述有點(diǎn)拗口,換一種說法:之前_owner指向的BasicLock在當(dāng)前線程棧上,說明當(dāng)前線程是第一次進(jìn)入該monitor,設(shè)置_recursions為1,_owner為當(dāng)前線程,該線程成功獲得鎖并返回;
4、如果獲取鎖失敗,則等待鎖的釋放;

monitor等待

monitor競(jìng)爭(zhēng)失敗的線程,通過自旋執(zhí)行ObjectMonitor::EnterI方法等待鎖的釋放,EnterI方法的部分邏輯實(shí)現(xiàn)如下:

image

1、當(dāng)前線程被封裝成ObjectWaiter對(duì)象node,狀態(tài)設(shè)置成ObjectWaiter::TS_CXQ;
2、在for循環(huán)中,通過CAS把node節(jié)點(diǎn)push到_cxq列表中,同一時(shí)刻可能有多個(gè)線程把自己的node節(jié)點(diǎn)push到_cxq列表中;
3、node節(jié)點(diǎn)push到_cxq列表之后,通過自旋嘗試獲取鎖,如果還是沒有獲取到鎖,則通過park將當(dāng)前線程掛起,等待被喚醒,實(shí)現(xiàn)如下:

image

4、當(dāng)該線程被喚醒時(shí),會(huì)從掛起的點(diǎn)繼續(xù)執(zhí)行,通過ObjectMonitor::TryLock嘗試獲取鎖,TryLock方法實(shí)現(xiàn)如下:

image

其本質(zhì)就是通過CAS設(shè)置monitor的_owner字段為當(dāng)前線程,如果CAS成功,則表示該線程獲取了鎖,跳出自旋操作,執(zhí)行同步代碼,否則繼續(xù)被掛起;

monitor釋放

當(dāng)某個(gè)持有鎖的線程執(zhí)行完同步代碼塊時(shí),會(huì)進(jìn)行鎖的釋放,給其它線程機(jī)會(huì)執(zhí)行同步代碼,在HotSpot中,通過退出monitor的方式實(shí)現(xiàn)鎖的釋放,并通知被阻塞的線程,具體實(shí)現(xiàn)位于ObjectMonitor::exit方法中。

image

1、如果是重量級(jí)鎖的釋放,monitor中的_owner指向當(dāng)前線程,即THREAD == _owner;
2、根據(jù)不同的策略(由QMode指定),從cxq或EntryList中獲取頭節(jié)點(diǎn),通過ObjectMonitor::ExitEpilog方法喚醒該節(jié)點(diǎn)封裝的線程,喚醒操作最終由unpark完成,實(shí)現(xiàn)如下:

image

3、被喚醒的線程,繼續(xù)執(zhí)行monitor的競(jìng)爭(zhēng);

作者:占小狼
鏈接:http://www.itdecent.cn/p/c5058b6fe8e5
來源:簡(jiǎn)書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容