溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java編程中synchronized的實現(xiàn)原理

發(fā)布時間:2021-08-27 17:17:23 來源:億速云 閱讀:160 作者:chen 欄目:開發(fā)技術(shù)

這篇文章主要講解了“Java編程中synchronized的實現(xiàn)原理”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Java編程中synchronized的實現(xiàn)原理”吧!

目錄
  • synchronized的三種應(yīng)用方式

    • synchronized作用于實例方法

    • synchronized作用于靜態(tài)方法

    • synchronized同步代碼塊

  • synchronized底層語義原理

    • 理解Java對象頭與Monitor

    • synchronized代碼塊底層原理

    • synchronized方法底層原理

    • Java虛擬機對synchronized的優(yōu)化

      • 偏向鎖

      • 輕量級鎖

      • 自旋鎖

      • 鎖消除

  • 關(guān)于synchronized 可能需要了解的關(guān)鍵點

    • synchronized的可重入性

      • 線程中斷與synchronized

        • 線程中斷

        • 中斷與synchronized

      • 等待喚醒機制與synchronized


    synchronized的三種應(yīng)用方式

    synchronized關(guān)鍵字最主要有以下3種應(yīng)用方式,下面分別介紹

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

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

    • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖。

    synchronized作用于實例方法

    所謂的實例對象鎖就是用synchronized修飾實例對象中的實例方法,注意是實例方法不包括靜態(tài)方法,如下

    public class AccountingSync implements Runnable{
        //共享資源(臨界資源)
        static int i=0;
        /**
         * synchronized 修飾實例方法
         */
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            AccountingSync instance=new AccountingSync();
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
        /**
         * 輸出結(jié)果:
         * 2000000
         */
    }

    上述代碼中,我們開啟兩個線程操作同一個共享資源即變量i,由于i++;操作并不具備原子性,該操作是先讀取值,然后寫回一個新值,相當(dāng)于原來的值加上1,分兩步完成,如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那么第二個線程就會與第一個線程一起看到同一個值,并執(zhí)行相同值的加1操作,這也就造成了線程安全失敗,因此對于increase方法必須使用synchronized修飾,以便保證線程安全。此時我們應(yīng)該注意到synchronized修飾的是實例方法increase,在這樣的情況下,當(dāng)前線程的鎖便是實例對象instance,注意Java中的線程同步鎖可以是任意對象。從代碼執(zhí)行結(jié)果來看確實是正確的,倘若我們沒有使用synchronized關(guān)鍵字,其最終輸出結(jié)果就很可能小于2000000,這便是synchronized關(guān)鍵字的作用。這里我們還需要意識到,當(dāng)一個線程正在訪問一個對象的 synchronized 實例方法,那么其他線程不能訪問該對象的其他 synchronized 方法,畢竟一個對象只有一把鎖,當(dāng)一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized實例方法,但是其他線程還是可以訪問該實例對象的其他非synchronized方法,當(dāng)然如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當(dāng)前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當(dāng)前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖并不同相同,此時如果兩個線程操作數(shù)據(jù)并非共享的,線程安全是有保障的,遺憾的是如果兩個線程操作的是共享數(shù)據(jù),那么線程安全就有可能無法保證了,如下代碼將演示出該現(xiàn)象

    public class AccountingSyncBad implements Runnable{
        static int i=0;
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //new新實例
            Thread t1=new Thread(new AccountingSyncBad());
            //new新實例
            Thread t2=new Thread(new AccountingSyncBad());
            t1.start();
            t2.start();
            //join含義:當(dāng)前線程A等待thread線程終止之后才能從thread.join()返回
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }

    上述代碼與前面不同的是我們同時創(chuàng)建了兩個新實例AccountingSyncBad,然后啟動兩個不同的線程對共享變量i進(jìn)行操作,但很遺憾操作結(jié)果是1452317而不是期望結(jié)果2000000,因為上述代碼犯了嚴(yán)重的錯誤,雖然我們使用synchronized修飾了increase方法,但卻new了兩個不同的實例對象,這也就意味著存在著兩個不同的實例對象鎖,因此t1和t2都會進(jìn)入各自的對象鎖,也就是說t1和t2線程使用的是不同的鎖,因此線程安全是無法保證的。解決這種困境的的方式是將synchronized作用于靜態(tài)的increase方法,這樣的話,對象鎖就當(dāng)前類對象,由于無論創(chuàng)建多少個實例對象,但對于的類對象擁有只有一個,所有在這樣的情況下對象鎖就是唯一的。下面我們看看如何使用將synchronized作用于靜態(tài)的increase方法。

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

    當(dāng)synchronized作用于靜態(tài)方法時,其鎖就是當(dāng)前類的class對象鎖。由于靜態(tài)成員不專屬于任何一個實例對象,是類成員,因此通過class對象鎖可以控制靜態(tài) 成員的并發(fā)操作。需要注意的是如果一個線程A調(diào)用一個實例對象的非static synchronized方法,而線程B需要調(diào)用這個實例對象所屬類的靜態(tài) synchronized方法,是允許的,不會發(fā)生互斥現(xiàn)象,因為訪問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的class對象,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實例對象鎖,看如下代碼

    public class AccountingSyncClass implements Runnable{
        static int i=0;
        /**
         * 作用于靜態(tài)方法,鎖是當(dāng)前class對象,也就是
         * AccountingSyncClass類對應(yīng)的class對象
         */
        public static synchronized void increase(){
            i++;
        }
        /**
         * 非靜態(tài),訪問時鎖不一樣不會發(fā)生互斥
         */
        public synchronized void increase4Obj(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //new新實例
            Thread t1=new Thread(new AccountingSyncClass());
            //new心事了
            Thread t2=new Thread(new AccountingSyncClass());
            //啟動線程
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }

    由于synchronized關(guān)鍵字修飾的是靜態(tài)increase方法,與修飾實例方法不同的是,其鎖對象是當(dāng)前類的class對象。注意代碼中的increase4Obj方法是實例方法,其對象鎖是當(dāng)前實例對象,如果別的線程調(diào)用該方法,將不會產(chǎn)生互斥現(xiàn)象,畢竟鎖對象不同,但我們應(yīng)該意識到這種情況下可能會發(fā)現(xiàn)線程安全問題(操作了共享靜態(tài)變量i)。

    synchronized同步代碼塊

    除了使用關(guān)鍵字修飾實例方法和靜態(tài)方法外,還可以使用同步代碼塊,在某些情況下,我們編寫的方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的代碼又只有一小部分,如果直接對整個方法進(jìn)行同步操作,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進(jìn)行包裹,這樣就無需對整個方法進(jìn)行同步操作了,同步代碼塊的使用示例如下:

    public class AccountingSync implements Runnable{
        static AccountingSync instance=new AccountingSync();
        static int i=0;
        @Override
        public void run() {
            //省略其他耗時操作....
            //使用同步代碼塊對變量i進(jìn)行同步操作,鎖對象為instance
            synchronized(instance){
                for(int j=0;j<1000000;j++){
                        i++;
                  }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }

    從代碼看出,將synchronized作用于一個給定的實例對象instance,即當(dāng)前實例對象就是鎖對象,每次當(dāng)線程進(jìn)入synchronized包裹的代碼塊時就會要求當(dāng)前線程持有instance實例對象鎖,如果當(dāng)前有其他線程正持有該對象鎖,那么新到的線程就必須等待,這樣也就保證了每次只有一個線程執(zhí)行i++;操作。當(dāng)然除了instance作為對象外,我們還可以使用this對象(代表當(dāng)前實例)或者當(dāng)前類的class對象作為鎖,如下代碼:

    //this,當(dāng)前實例對象鎖
    synchronized(this){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }
    //class對象鎖
    synchronized(AccountingSync.class){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }

    了解完synchronized的基本含義及其使用方式后,下面我們將進(jìn)一步深入理解synchronized的底層實現(xiàn)原理。

    synchronized底層語義原理

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

    理解Java對象頭與Monitor

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

    Java編程中synchronized的實現(xiàn)原理

    • 實例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內(nèi)存按4字節(jié)對齊。

    • 填充數(shù)據(jù):由于虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊,這點了解即可。

    而對于頂部,則是Java頭對象,它實現(xiàn)synchronized的鎖對象的基礎(chǔ),這點我們重點分析它,一般而言,synchronized使用的鎖對象是存儲在Java對象頭里的,jvm中采用2個字來存儲對象頭(如果對象是數(shù)組則會分配3個字,多出來的1個字記錄的是數(shù)組長度),其主要結(jié)構(gòu)是由Mark Word 和 Class Metadata Address 組成,其結(jié)構(gòu)說明如下表:

    虛擬機位數(shù)頭對象結(jié)構(gòu)說明
    32/64bitMark Word存儲對象的hashCode、鎖信息或分代年齡或GC標(biāo)志等信息
    32/64bitClass Metadata Address類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過這個指針確定該對象是哪個類的實例。

    其中Mark Word在默認(rèn)情況下存儲著對象的HashCode、分代年齡、鎖標(biāo)記位等以下是32位JVM的Mark Word默認(rèn)存儲結(jié)構(gòu)

    鎖狀態(tài)25bit4bit1bit是否是偏向鎖2bit 鎖標(biāo)志位
    無鎖狀態(tài)對象HashCode對象分代年齡001

    由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設(shè)計成為一個非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲更多有效的數(shù)據(jù),它會根據(jù)對象本身的狀態(tài)復(fù)用自己的存儲空間,如32位JVM下,除了上述列出的Mark Word默認(rèn)存儲結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):

    Java編程中synchronized的實現(xiàn)原理

    其中輕量級鎖和偏向鎖是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虛擬機(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現(xiàn)的)

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //記錄個數(shù)
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;
        _WaitSet      = NULL; //處于wait狀態(tài)的線程,會被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //處于等待鎖block狀態(tài)的線程,會被加入到該列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }

    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)入 WaitSe t集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。如下圖所示

    Java編程中synchronized的實現(xiàn)原理

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

    synchronized代碼塊底層原理

    現(xiàn)在我們重新定義一個synchronized修飾的同步代碼塊,在代碼塊中操作共享變量i,如下

    public class SyncCodeBlock {
       public int i;
       public void syncTask(){
           //同步代碼庫
           synchronized (this){
               i++;
           }
       }
    }

    編譯上述代碼并使用javap反編譯后得到字節(jié)碼如下(這里我們省略一部分沒有必要的信息):

    Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
      Last modified 2017-6-2; size 426 bytes
      MD5 checksum c80bc322c87b312de760942820b4fed5
      Compiled from "SyncCodeBlock.java"
    public class com.zejian.concurrencys.SyncCodeBlock
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
      //........省略常量池中數(shù)據(jù)
      //構(gòu)造函數(shù)
      public com.zejian.concurrencys.SyncCodeBlock();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 7: 0
      //===========主要看看syncTask方法實現(xiàn)================
      public void syncTask();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=3, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter  //注意此處,進(jìn)入同步方法
             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   //注意此處,退出同步方法
            16: goto          24
            19: astore_2
            20: aload_1
            21: monitorexit //注意此處,退出同步方法
            22: aload_2
            23: athrow
            24: return
          Exception table:
          //省略其他字節(jié)碼.......
    }
    SourceFile: "SyncCodeBlock.java"

    我們主要關(guān)注字節(jié)碼中的如下代碼

    3: monitorenter //進(jìn)入同步方法
    //..........省略其他
    15: monitorexit //退出同步方法
    16: goto 24
    //省略其他.......
    21: monitorexit //退出同步方法

    從字節(jié)碼中可知同步語句塊的實現(xiàn)使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結(jié)束位置,當(dāng)執(zhí)行monitorenter指令時,當(dāng)前線程將試圖獲取 objectref(即對象鎖) 所對應(yīng)的 monitor 的持有權(quán),當(dāng) objectref 的 monitor 的進(jìn)入計數(shù)器為 0,那線程可以成功取得 monitor,并將計數(shù)器值設(shè)置為 1,取鎖成功。如果當(dāng)前線程已經(jīng)擁有 objectref 的 monitor 的持有權(quán),那它可以重入這個 monitor (關(guān)于重入性稍后會分析),重入時計數(shù)器的值也會加 1。倘若其他線程已經(jīng)擁有 objectref 的 monitor 的所有權(quán),那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢,即monitorexit指令被執(zhí)行,執(zhí)行線程將釋放 monitor(鎖)并設(shè)置計數(shù)器值為0 ,其他線程將有機會持有 monitor 。值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對應(yīng) monitorexit 指令,而無論這個方法是正常結(jié)束還是異常結(jié)束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行,編譯器會自動產(chǎn)生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令。從字節(jié)碼中也可以看出多了一個monitorexit指令,它就是異常結(jié)束時被執(zhí)行的釋放monitor 的指令。

    synchronized方法底層原理

    方法級的同步是隱式,即無需通過字節(jié)碼指令來控制的,它實現(xiàn)在方法調(diào)用和返回操作之中。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標(biāo)志區(qū)分一個方法是否同步方法。當(dāng)方法調(diào)用時,調(diào)用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有monitor(虛擬機規(guī)范中用的是管程一詞), 然后再執(zhí)行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執(zhí)行期間拋 出了異常,并且在方法內(nèi)部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。下面我們看看字節(jié)碼層面如何實現(xiàn):

    public class SyncMethod {
       public int i;
       public synchronized void syncTask(){
               i++;
       }
    }

    使用javap反編譯后的字節(jié)碼如下:

    Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
      Last modified 2017-6-2; size 308 bytes
      MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
      Compiled from "SyncMethod.java"
    public class com.zejian.concurrencys.SyncMethod
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool;
       //省略沒必要的字節(jié)碼
      //==================syncTask方法======================
      public synchronized void syncTask();
        descriptor: ()V
        //方法標(biāo)識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
        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
          LineNumberTable:
            line 12: 0
            line 13: 10
    }
    SourceFile: "SyncMethod.java"

    從字節(jié)碼中可以看出,synchronized修飾的方法并沒有monitorenter指令和monitorexit指令,取得代之的確實是ACC_SYNCHRONIZED標(biāo)識,該標(biāo)識指明了該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED訪問標(biāo)志來辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。這便是synchronized鎖在同步代碼塊和同步方法上實現(xiàn)的基本原理。同時我們還必須注意到的是在Java早期版本中,synchronized屬于重量級鎖,效率低下,因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的synchronized效率低的原因。慶幸的是在Java 6之后Java官方對從JVM層面對synchronized較大優(yōu)化,所以現(xiàn)在的synchronized鎖效率也優(yōu)化得很不錯了,Java 6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖,接下來我們將簡單了解一下Java官方在JVM層面對synchronized鎖的優(yōu)化。

    Java虛擬機對synchronized的優(yōu)化

    鎖的狀態(tài)總共有四種,無鎖狀態(tài)、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現(xiàn)鎖的降級,關(guān)于重量級鎖,前面我們已詳細(xì)分析過,下面我們將介紹偏向鎖和輕量級鎖以及JVM的其他優(yōu)化手段,這里并不打算深入到每個鎖的實現(xiàn)和轉(zhuǎn)換過程更多地是闡述Java虛擬機所提供的每個鎖的核心優(yōu)化思想,畢竟涉及到具體過程比較繁瑣,如需了解詳細(xì)過程可以查閱《深入理解Java虛擬機原理》。

    偏向鎖

    偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優(yōu)化手段,經(jīng)過研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關(guān)鎖申請的操作,從而也就提供程序的性能。所以,對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應(yīng)該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接著了解輕量級鎖。

    輕量級鎖

    倘若偏向鎖失敗,虛擬機并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優(yōu)化手段(1.6之后加入的),此時Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級鎖的結(jié)構(gòu)。輕量級鎖能夠提升程序性能的依據(jù)是“對絕大部分的鎖,在整個同步周期內(nèi)都不存在競爭”,注意這是經(jīng)驗數(shù)據(jù)。需要了解的是,輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導(dǎo)致輕量級鎖膨脹為重量級鎖。

    自旋鎖

    輕量級鎖失敗后,虛擬機為了避免線程真實地在操作系統(tǒng)層面掛起,還會進(jìn)行一項稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統(tǒng)層面的線程可能會得不償失,畢竟操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設(shè)在不久將來,當(dāng)前的線程可以獲得鎖,因此虛擬機會讓當(dāng)前想要獲取鎖的線程做幾個空循環(huán)(這也是稱為自旋的原因),一般不會太久,可能是50個循環(huán)或100循環(huán),在經(jīng)過若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實也是可以提升效率的。最后沒辦法也就只能升級為重量級鎖了。

    鎖消除

    消除鎖是虛擬機另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當(dāng)某段代碼即將第一次被執(zhí)行時進(jìn)行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量,并且不會被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。

    /**
     * Created by zejian on 2017/6/4.
     * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創(chuàng)]
     * 消除StringBuffer同步鎖
     */
    public class StringBufferRemoveSync {
        public void add(String str1, String str2) {
            //StringBuffer是線程安全,由于sb只會在append方法中使用,不可能被其他線程引用
            //因此sb屬于不可能共享的資源,JVM會自動消除內(nèi)部的鎖
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
        }
        public static void main(String[] args) {
            StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
            for (int i = 0; i < 10000000; i++) {
                rmsync.add("abc", "123");
            }
        }
    }

    關(guān)于synchronized 可能需要了解的關(guān)鍵點

    synchronized的可重入性

    從互斥鎖的設(shè)計上來說,當(dāng)一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處于阻塞狀態(tài),但當(dāng)一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖,請求將會成功,在java中synchronized是基于原子性的內(nèi)部鎖機制,是可重入的,因此在一個線程調(diào)用synchronized方法的同時在其方法體內(nèi)部調(diào)用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖后再次請求該對象鎖,是允許的,這就是synchronized的可重入性。如下:

    public class AccountingSync implements Runnable{
        static AccountingSync instance=new AccountingSync();
        static int i=0;
        static int j=0;
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                //this,當(dāng)前實例對象鎖
                synchronized(this){
                    i++;
                    increase();//synchronized的可重入性
                }
            }
        }
        public synchronized void increase(){
            j++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }

    正如代碼所演示的,在獲取當(dāng)前實例對象鎖后進(jìn)入synchronized代碼塊執(zhí)行同步代碼,并在代碼塊中調(diào)用了當(dāng)前實例對象的另外一個synchronized方法,再次請求當(dāng)前實例鎖時,將被允許,進(jìn)而執(zhí)行方法體代碼,這就是重入鎖最直接的體現(xiàn),需要特別注意另外一種情況,當(dāng)子類繼承父類時,子類也是可以通過可重入鎖調(diào)用父類的同步方法。注意由于synchronized是基于monitor實現(xiàn)的,因此每次重入,monitor中的計數(shù)器仍會加1。

    線程中斷與synchronized

    線程中斷

    正如中斷二字所表達(dá)的意義,在線程運行(run方法)中間打斷它,在Java中,提供了以下3個有關(guān)線程中斷的方法

    //中斷線程(實例方法)
    public void Thread.interrupt();
    //判斷線程是否被中斷(實例方法)
    public boolean Thread.isInterrupted();
    //判斷是否被中斷并清除當(dāng)前中斷狀態(tài)(靜態(tài)方法)
    public static boolean Thread.interrupted();

    當(dāng)一個線程處于被阻塞狀態(tài)或者試圖執(zhí)行一個阻塞操作時,使用Thread.interrupt()方式中斷該線程,注意此時將會拋出一個InterruptedException的異常,同時中斷狀態(tài)將會被復(fù)位(由中斷狀態(tài)改為非中斷狀態(tài)),如下代碼將演示該過程:

    public class InterruputSleepThread3 {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    //while在try中,通過異常中斷就可以退出run循環(huán)
                    try {
                        while (true) {
                            //當(dāng)前線程處于阻塞狀態(tài),異常必須捕捉處理,無法往外拋出
                            TimeUnit.SECONDS.sleep(2);
                        }
                    } catch (InterruptedException e) {
                        System.out.println("Interruted When Sleep");
                        boolean interrupt = this.isInterrupted();
                        //中斷狀態(tài)被復(fù)位
                        System.out.println("interrupt:"+interrupt);
                    }
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            //中斷處于阻塞狀態(tài)的線程
            t1.interrupt();
            /**
             * 輸出結(jié)果:
               Interruted When Sleep
               interrupt:false
             */
        }
    }

    如上述代碼所示,我們創(chuàng)建一個線程,并在線程中調(diào)用了sleep方法從而使用線程進(jìn)入阻塞狀態(tài),啟動線程后,調(diào)用線程實例對象的interrupt方法中斷阻塞異常,并拋出InterruptedException異常,此時中斷狀態(tài)也將被復(fù)位。這里有些人可能會詫異,為什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其實原因很簡單,前者使用時并沒有明確的單位說明,而后者非常明確表達(dá)秒的單位,事實上后者的內(nèi)部實現(xiàn)最終還是調(diào)用了Thread.sleep(2000);,但為了編寫的代碼語義更清晰,建議使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是個枚舉類型。ok~,除了阻塞中斷的情景,我們還可能會遇到處于運行期且非阻塞的狀態(tài)的線程,這種情況下,直接調(diào)用Thread.interrupt()中斷線程是不會得到任響應(yīng)的,如下代碼,將無法中斷非阻塞狀態(tài)下的線程:

    public class InterruputThread {
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(){
                @Override
                public void run(){
                    while(true){
                        System.out.println("未被中斷");
                    }
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.interrupt();
            /**
             * 輸出結(jié)果(無限執(zhí)行):
                 未被中斷
                 未被中斷
                 未被中斷
                 ......
             */
        }
    }

    雖然我們調(diào)用了interrupt方法,但線程t1并未被中斷,因為處于非阻塞狀態(tài)的線程需要我們手動進(jìn)行中斷檢測并結(jié)束程序,改進(jìn)后代碼如下:

    public class InterruputThread {
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(){
                @Override
                public void run(){
                    while(true){
                        //判斷當(dāng)前線程是否被中斷
                        if (this.isInterrupted()){
                            System.out.println("線程中斷");
                            break;
                        }
                    }
                    System.out.println("已跳出循環(huán),線程中斷!");
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.interrupt();
            /**
             * 輸出結(jié)果:
                線程中斷
                已跳出循環(huán),線程中斷!
             */
        }
    }

    是的,我們在代碼中使用了實例方法isInterrupted判斷線程是否已被中斷,如果被中斷將跳出循環(huán)以此結(jié)束線程,注意非阻塞狀態(tài)調(diào)用interrupt()并不會導(dǎo)致中斷狀態(tài)重置。綜合所述,可以簡單總結(jié)一下中斷兩種情況,一種是當(dāng)線程處于阻塞狀態(tài)或者試圖執(zhí)行一個阻塞操作時,我們可以使用實例方法interrupt()進(jìn)行線程中斷,執(zhí)行中斷操作后將會拋出interruptException異常(該異常必須捕捉無法向外拋出)并將中斷狀態(tài)復(fù)位,另外一種是當(dāng)線程處于運行狀態(tài)時,我們也可調(diào)用實例方法interrupt()進(jìn)行線程中斷,但同時必須手動判斷中斷狀態(tài),并編寫中斷線程的代碼(其實就是結(jié)束run方法體的代碼)。有時我們在編碼時可能需要兼顧以上兩種情況,那么就可以如下編寫:

    public void run(){
        try {
        //判斷當(dāng)前線程是否已中斷,注意interrupted方法是靜態(tài)的,執(zhí)行后會對中斷狀態(tài)進(jìn)行復(fù)位
        while (!Thread.interrupted()) {
            TimeUnit.SECONDS.sleep(2);
        }
        } catch (InterruptedException e) {
        }
    }
    中斷與synchronized

    事實上線程的中斷操作對于正在等待獲取的鎖對象的synchronized方法或者代碼塊并不起作用,也就是對于synchronized來說,如果一個線程在等待鎖,那么結(jié)果只有兩種,要么它獲得這把鎖繼續(xù)執(zhí)行,要么它就保存等待,即使調(diào)用中斷線程的方法,也不會生效。演示代碼如下

    /**
     * Created by zejian on 2017/6/2.
     * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創(chuàng)]
     */
    public class SynchronizedBlocked implements Runnable{
        public synchronized void f() {
            System.out.println("Trying to call f()");
            while(true) // Never releases lock
                Thread.yield();
        }
        /**
         * 在構(gòu)造器中創(chuàng)建新線程并啟動獲取對象鎖
         */
        public SynchronizedBlocked() {
            //該線程已持有當(dāng)前實例鎖
            new Thread() {
                public void run() {
                    f(); // Lock acquired by this thread
                }
            }.start();
        }
        public void run() {
            //中斷判斷
            while (true) {
                if (Thread.interrupted()) {
                    System.out.println("中斷線程!!");
                    break;
                } else {
                    f();
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            SynchronizedBlocked sync = new SynchronizedBlocked();
            Thread t = new Thread(sync);
            //啟動后調(diào)用f()方法,無法獲取當(dāng)前實例鎖處于等待狀態(tài)
            t.start();
            TimeUnit.SECONDS.sleep(1);
            //中斷線程,無法生效
            t.interrupt();
        }
    }

    我們在SynchronizedBlocked構(gòu)造函數(shù)中創(chuàng)建一個新線程并啟動獲取調(diào)用f()獲取到當(dāng)前實例鎖,由于SynchronizedBlocked自身也是線程,啟動后在其run方法中也調(diào)用了f(),但由于對象鎖被其他線程占用,導(dǎo)致t線程只能等到鎖,此時我們調(diào)用了t.interrupt();但并不能中斷線程。

    等待喚醒機制與synchronized

    所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處于synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常,這是因為調(diào)用這幾個方法前必須拿到當(dāng)前對象的監(jiān)視器monitor對象,也就是說notify/notifyAll和wait方法依賴于monitor對象,在前面的分析中,我們知道m(xù)onitor 存在于對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關(guān)鍵字可以獲取 monitor ,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調(diào)用的原因。

    synchronized (obj) {
           obj.wait();
           obj.notify();
           obj.notifyAll();         
     }

    需要特別理解的一點是,與sleep方法不同的是wait方法調(diào)用完成后,線程將被暫停,但wait方法將會釋放當(dāng)前持有的監(jiān)視器鎖(monitor),直到有線程調(diào)用notify/notifyAll方法后方能繼續(xù)執(zhí)行,而sleep方法只讓線程休眠并不釋放鎖。同時notify/notifyAll方法調(diào)用后,并不會馬上釋放監(jiān)視器鎖,而是在相應(yīng)的synchronized(){}/synchronized方法執(zhí)行結(jié)束后才自動釋放鎖。

    感謝各位的閱讀,以上就是“Java編程中synchronized的實現(xiàn)原理”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對Java編程中synchronized的實現(xiàn)原理這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI