多線程知識梳理(2) - synchronized 三部曲之基本使用

一、為什么要使用 synchronized

使用synchronized的原因在于:它能夠確保多個線程在同一時刻,只能有一個線程處于方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性。

二、synchronized 原理

JDK 1.6之前,synchronized的實現(xiàn)是基于對象上的監(jiān)視器,這也被稱為重量鎖。默認情況下,每一個對象都有一個關(guān)聯(lián)的Monitor,而每個Monitor包含了一個EntryCount計數(shù)器,它是synchronized實現(xiàn)可重入的關(guān)鍵。

JDK 1.6之后,對鎖進行了一系列優(yōu)化的措施,通過引入自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術(shù)來減少鎖操作的開銷。

這些優(yōu)化措施最終的目的是減少鎖操作的開銷,然而它所改變的只是鎖的實現(xiàn)方式,但是加鎖和解鎖這一基本原則是沒有改變的。這篇文章主要是介紹synchronized的使用,因此,在后面的介紹中,我們還是按照比較容易理解的重量鎖的方式進行分析,在之后的文章中,我們再來談一下優(yōu)化后的實現(xiàn)策略。

2.1 進入同步方法或者代碼塊

當一個線程執(zhí)行某個對象的同步方法或者代碼塊時,會先檢查這個對象所關(guān)聯(lián)的Monitor's EntryCount是否為0

  • 如果EntryCount0,那么該線程就會將Monitor’s EntryCount設(shè)置為1,并成為該Monitor的所有者,接著執(zhí)行該方法或者代碼塊中的語句。
  • 如果EntryCount不為0,這時會去檢查對象所關(guān)聯(lián)的Monitor的持有者是哪一個線程:
  • 第一種情況:持有該Monitor的線程就是當前正在嘗試獲取Monitor的線程,那么將EntryCount的數(shù)值加1,繼續(xù)執(zhí)行方法或者代碼塊中的語句。
  • 第二種情況:持有該Monitor的是其它的線程,那么該線程進入阻塞狀態(tài),直到EntryCount的數(shù)值變?yōu)?code>0。

2.2 退出同步方法或者代碼塊

當一個線程從同步方法或者代碼塊退出時,會將EntryCount1,如果EntryCount變?yōu)?code>0,那么該線程會釋放它所持有的Monitor。之前那些阻塞在synchronized的線程會嘗試去獲取Monitor,成功獲取Monitor的線程可以進入同步方法或者代碼塊。

三、synchronized 使用

對于synchronized的使用,我們有兩種分類方法:

  • 根據(jù)使用場景分類
  • 根據(jù)Monitor關(guān)聯(lián)的對象分類。

3.1 根據(jù)使用場景分類

很多介紹synchronized的文章,都是通過使用場景進行分類的,一般來說可以分為如下四種使用場景,而每種場景下根據(jù)Monitor所關(guān)聯(lián)的對象不同,又會衍生出另外的用法:

  • 靜態(tài)方法
    //靜態(tài)方法,使用的是Class類鎖
    synchronized public static void staticMethod() {}
  • 靜態(tài)方法代碼塊
    private static final byte[] mStaticLockByte = new byte[1];

    //靜態(tài)方法代碼塊1,使用的是Class類鎖
    public static void staticBlock1() {
        synchronized (SynchronizedObject.class) {}
    }

    //靜態(tài)方法代碼塊2,使用的是內(nèi)部靜態(tài)變量鎖
    public static void staticBlock2() {
        synchronized (mStaticLockByte) {} 
    }
  • 普通方法
    //普通方法,使用的是調(diào)用該方法的對象鎖
    synchronized public void method() {}
  • 普通方法代碼塊
    private static final byte[] mStaticLockByte = new byte[1];
    private final byte[] mLockByte = new byte[1];

    //普通方法代碼塊1,使用的是Class類鎖
    public void block1() {
        synchronized (SynchronizedObject.class) {}
    }

    //普通方法代碼塊2,使用的是mLockByte的變量鎖
    public void block2() {
        synchronized (mLockByte) {} //變量需要聲明為final
    }
    
    //普通方法代碼塊3,使用的是mStaticLockByte的變量鎖
    public void block3() {
        synchronized (mStaticLockByte) {} 
    }

    //普通方法代碼塊4,使用的是調(diào)用該方法的對象鎖
    public void block4() {
        synchronized (this) {}
    }

3.2 根據(jù) Monitor 關(guān)聯(lián)的對象分類

根據(jù)使用場景進行分類,主要是為了讓大家知道如何使用synchronized關(guān)鍵字,然而要真正地理解synchronized,就需要結(jié)合第二節(jié)談到的synchronized原理,其實3.1中談到的多種場景,都是和Monitor有關(guān),那么從和Monitor關(guān)聯(lián)的對象來看,我們重新對3.1中的8種場景重新進行分類:

  • Class對象:
  • 靜態(tài)方法
  • 靜態(tài)方法代碼塊1 - SynchronizedObject.class
  • 普通方法代碼塊1 - SynchronizedObject.class
  • 調(diào)用方法的對象
  • 普通方法
  • 普通方法代碼塊4 - this
  • 靜態(tài)對象
  • 靜態(tài)方法代碼塊2 - mStaticLockByte
  • 普通方法代碼塊3 - mStaticLockByte
  • 非靜態(tài)對象
  • 普通方法代碼塊1 - mLockByte

如果使用場景屬于上面的同一個分類當中,那么才有可能產(chǎn)生線程阻塞在synchronized關(guān)鍵字的情況,舉一個例子,如果A線程通過靜態(tài)方法訪問(分類一)并且沒有從該方法退出:

  • 這時B線程是通過一個對象的普通方法來訪問(分類二),那么是不會阻塞的,這是因為調(diào)用該方法的對象所關(guān)聯(lián)的Monitor沒有被持有。
  • 如果B線程使用的是靜態(tài)方法代碼塊來訪問,而該靜態(tài)方法代碼塊使用的是SynchronizedObject.class來修飾(分類一),由于這兩種使用場景是屬于同一個分類,那么就會B線程就會進入阻塞狀態(tài),這是因為SynchronizedObject類所關(guān)聯(lián)的Monitor已經(jīng)被A線程持有了。

四、小結(jié)

從表面上來看,synchronized的使用可以簡單地分為同步方法和同步代碼塊,但是究竟在什么情況下會導(dǎo)致一個線程在synchronized上阻塞,則需要分析synchronized方法所嘗試獲取的Monitor的是否已經(jīng)被其它線程持有了。

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

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

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