Java多線程之synchronized實現(xiàn)原理

一、synchronized簡介

在并發(fā)編程中多個線程同時操作同一個資源,極易導(dǎo)致錯誤數(shù)據(jù)的產(chǎn)生。因此為了解決這個問題,當(dāng)存在多個線程操作共享數(shù)據(jù)時,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行。

在Java中,關(guān)鍵字synchronized可以保證在同一個時刻,只有一個線程可以執(zhí)行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時我們還應(yīng)該注意到synchronized另外一個重要的作用,synchronized可保證一個線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。

二、synchronized應(yīng)用方式

synchronized主要有以下三種使用方式

  1. 作用于實例方法,當(dāng)前實例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實例的鎖;

  2. 作用于靜態(tài)方法,當(dāng)前類加鎖,進(jìn)去同步代碼前要獲得當(dāng)前類對象的鎖;

  3. 作用于代碼塊,這需要指定加鎖的對象,對所給的指定對象加鎖,進(jìn)入同步代碼前要獲得指定對象的鎖。

1、作用于實例方法

public class SynchronizedMethodTest implements Runnable {

    private int i = 0;
    private static int TOTAL = 1000;

    public synchronized void add() {
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < TOTAL; j++) {
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedMethodTest s = new SynchronizedMethodTest();
        Thread a = new Thread(s, "線程A");
        Thread b = new Thread(s, "線程B");
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.printf("i=%s", s.i);
    }

}
/**
 * 輸出結(jié)果: i=2000
 */

2、作用于靜態(tài)方法

package com.dragon.thread.sync;

public class SynchronizedStaticMethodTest implements Runnable {

    private static int i = 0;
    private static int TOTAL = 1000;

    public synchronized static void add() {
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < TOTAL; j++) {
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedStaticMethodTest s1 = new SynchronizedStaticMethodTest();
        SynchronizedStaticMethodTest s2 = new SynchronizedStaticMethodTest();
        Thread a = new Thread(s1, "線程A");
        Thread b = new Thread(s2, "線程B");
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.printf("i=%s", i);
    }

}
/**
 * 輸出結(jié)果: i=2000
 */

3、作用于代碼塊

public class SynchronizedBlockTest implements Runnable {

    private int i = 0;
    private static int TOTAL = 1000;

    public void add() {
        synchronized (this) {
            i++;
        }
    }

    @Override
    public void run() {
        for (int j = 0; j < TOTAL; j++) {
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedBlockTest s = new SynchronizedBlockTest();
        Thread a = new Thread(s, "線程A");
        Thread b = new Thread(s, "線程B");
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.printf("i=%s", s.i);
    }

}
/**
 * 輸出結(jié)果: i=2000
 */

三、synchronized底層原理

Java 虛擬機(jī)中的同步Synchronization基于進(jìn)入和退出管程Monitor對象實現(xiàn), 無論是顯式同步(有明確的monitorentermonitorexit指令,即同步代碼塊)還是隱式同步都是如此。在 Java 語言中,同步用的最多的地方可能是被synchronized修飾的同步方法。同步方法 并不是由monitorentermonitorexit指令來實現(xiàn)同步的,而是由方法調(diào)用指令讀取運行時常量池中方法的ACC_SYNCHRONIZED標(biāo)志來隱式實現(xiàn)的,關(guān)于這點,稍后詳細(xì)分析。下面先來了解一個概念Java對象頭,這對深入理解synchronized實現(xiàn)原理非常關(guān)鍵。

1、理解Java對象頭與Monitor

在JVM中,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。


JAVA對象實例結(jié)構(gòu)

對象頭

HotSpot虛擬機(jī)的對象頭包括兩部分信息:

  1. markword
    第一部分markword,用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32bit64bit,官方稱它為MarkWord。
  2. klass
    對象頭的另外一部分是klass類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實例.
  3. 數(shù)組長度(只有數(shù)組對象有)
    如果對象是一個數(shù)組, 那在對象頭中還必須有一塊數(shù)據(jù)用于記錄數(shù)組長度.

實例數(shù)據(jù)

實例數(shù)據(jù)部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。

對齊填充

第三部分對齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說,就是對象的大小必須是8字節(jié)的整數(shù)倍。而對象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對象實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填充來補(bǔ)全。

32位虛擬機(jī)在不同狀態(tài)下markword結(jié)構(gòu)如下圖所示


markword結(jié)構(gòu)

其中輕量級鎖和偏向鎖是Java 6 對synchronized鎖進(jìn)行優(yōu)化后新增加的,稍后我們會簡要分析。這里我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標(biāo)識位為10,其中指針指向的是monitor對象(也稱為管程或監(jiān)視器鎖)的起始地址。每個對象都存在著一個monitor與之關(guān)聯(lián),對象與其monitor之間的關(guān)系有存在多種實現(xiàn)方式,如monitor可以與對象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對象鎖時自動生成,但當(dāng)一個monitor被某個線程持有后,它便處于鎖定狀態(tài)。在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件,C++實現(xiàn)的)

ObjectMonitor() {
    _header       = NULL;//markOop對象頭
    _count        = 0;
    _waiters      = 0,//等待線程數(shù)
    _recursions   = 0;//重入次數(shù)
    _object       = NULL;//監(jiān)視器鎖寄生的對象。鎖不是平白出現(xiàn)的,而是寄托存儲于對象中。
    _owner        = NULL;//初始時為NULL表示當(dāng)前沒有任何線程擁有該monitor record,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識,當(dāng)鎖被釋放時又設(shè)置為NULL
    _WaitSet      = NULL;//處于wait狀態(tài)的線程,會被加入到wait set;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;//處于等待鎖block狀態(tài)的線程,會被加入到entry set;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
  }

ObjectMonitor中有兩個隊列,_WaitSet_EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當(dāng)多個線程同時訪問一段同步代碼時,首先會進(jìn)入_EntryList集合,當(dāng)線程獲取到對象的monitor后進(jìn)入_Owner區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用wait()方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進(jìn)入WaitSet集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。如下圖所示

監(jiān)視器

由此看來,monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因(關(guān)于這點稍后還會進(jìn)行分析),ok~,有了上述知識基礎(chǔ)后,下面我們將進(jìn)一步分析synchronized在字節(jié)碼層面的具體語義實現(xiàn)。

2、同步方法的實現(xiàn)原理

使用javap -v SynchronizedMethodTest.class反編譯

/**
 * 此處省略大段代碼
 */

  public synchronized void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
/**
 * 此處省略大段代碼
 */
}
SourceFile: "SynchronizedMethodTest.java"

3、同步代碼塊的實現(xiàn)原理

使用javap -v SynchronizedBlockTest.class反編譯

/**
 * 此處省略大段代碼
 */
  public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter                    //申請獲得對象的內(nèi)置鎖
         4: aload_0
         5: dup
         6: getfield      #2                  // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field i:I
        14: aload_1
        15: monitorexit                       //釋放對象內(nèi)置鎖
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit                      //出現(xiàn)異常,釋放對象內(nèi)置鎖
        22: aload_2
        23: athrow
        24: return
/**
 * 此處省略大段代碼
 */
}
SourceFile: "SynchronizedBlockTest.java"

從上述指令我們可以得出以下結(jié)論:

  1. 同步代碼塊是使用monitorentermonitorexit指令實現(xiàn)的,會在同步塊的區(qū)域通過監(jiān)聽器對象去獲取鎖和釋放鎖,從而在字節(jié)碼層面來控制同步scope。
  2. 同步方法和靜態(tài)同步方法依靠的是方法修飾符上的ACC_SYNCHRONIZED實現(xiàn)。JVM根據(jù)該修飾符來實現(xiàn)方法的同步。當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的ACC_SYNCHRONIZED訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。

結(jié)束

最后編輯于
?著作權(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)容