本文轉載于SegmentFault社區
作者:ytao
synchronized是 Java 編程中的一個重要的關鍵字,也是多線程編程中不可或缺的一員。本文就對它的使用和鎖的一些重要概念進行分析。
使用及原理
synchronized 是一個重量級鎖,它主要實現同步操作,在 Java 對象鎖中有三種使用方式:
普通方法中使用,鎖是當前實例對象。
靜態方法中使用,鎖是當前類的對象。
代碼塊中使用,鎖是代碼代碼塊中配置的對象。
使用
在代碼中使用方法分別如下:
普通方法使用:
/** * 公眾號:ytao * 博客:https://ytao.top */public class SynchronizedMethodDemo{ public synchronized void demo(){ // ...... }}
靜態方法使用:
/** * 公眾號:ytao * 博客:https://ytao.top */public class SynchronizedMethodDemo{ public synchronized static void staticDemo(){ // ...... }}
代碼塊中使用:
/** * 公眾號:ytao * 博客:https://ytao.top */public class SynchronizedDemo{ public void demo(){ synchronized (SynchronizedDemo.class){ // ...... } }}
實現原理
方法和代碼塊的實現原理使用不同方式:
代碼塊
每個對象都擁有一個monitor對象,代碼塊的{}中會插入monitorenter和monitorexit指令。當執行monitorenter指令時,會進入monitor對象獲取鎖,當執行monitorexit命令時,會退出monitor對象釋放鎖。同一時刻,只能有一個線程進入在monitorenter中。
先將SynchronizedDemo.java使用javac SynchronizedDemo.java命令將其編譯成SynchronizedDemo.class。然后使用javap -c SynchronizedDemo.class反編譯字節碼。Compiled from "SynchronizedDemo.java"public class SynchronizedDemo { public SynchronizedDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public void demo(); Code: 0: ldc #2 // class SynchronizedDemo 2: dup 3: astore_1 4: monitorenter // 進入 monitor 5: aload_1 6: monitorexit // 退出 monitor 7: goto 15 10: astore_2 11: aload_1 12: monitorexit // 退出 monitor 13: aload_2 14: athrow 15: return Exception table: from to target type 5 7 10 any 10 13 10 any}
上面反編碼后的代碼,有兩個monitorexit指令,一個插入在異常位置,一個插入在方法結束位置。
方法
方法中的synchronized與代碼塊中實現的方式不同,方法中會添加一個叫ACC_SYNCHRONIZED的標志,當調用方法時,首先會檢查是否有ACC_SYNCHRONIZED標志,如果存在,則獲取monitor對象,調用monitorenter和monitorexit指令。
通過javap -v -c SynchronizedMethodDemo.class命令反編譯SynchronizedMethodDemo類。-v參數即-verbose,表示輸出反編譯的附加信息。下面以反編譯普通方法為例。
Classfile /E:/SynchronizedMethodDemo.class Last modified 2020-6-28; size 381 bytes MD5 checksum 55ca2bbd9b6939bbd515c3ad9e59d10c Compiled from "SynchronizedMethodDemo.java"public class SynchronizedMethodDemo minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #5.#13 // java/lang/Object."":()V #2 = Fieldref #14.#15 // java/lang/System.out:Ljava/io/PrintStream; #3 = Methodref #16.#17 // java/io/PrintStream.println:()V #4 = Class #18 // SynchronizedMethodDemo #5 = Class #19 // java/lang/Object #6 = Utf8 #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 demo #11 = Utf8 SourceFile #12 = Utf8 SynchronizedMethodDemo.java #13 = NameAndType #6:#7 // "":()V #14 = Class #20 // java/lang/System #15 = NameAndType #21:#22 // out:Ljava/io/PrintStream; #16 = Class #23 // java/io/PrintStream #17 = NameAndType #24:#7 // println:()V #18 = Utf8 SynchronizedMethodDemo #19 = Utf8 java/lang/Object #20 = Utf8 java/lang/System #21 = Utf8 out #22 = Utf8 Ljava/io/PrintStream; #23 = Utf8 java/io/PrintStream #24 = Utf8 println{ public SynchronizedMethodDemo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 5: 0 public synchronized void demo(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED 標志 Code: stack=1, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: invokevirtual #3 // Method java/io/PrintStream.println:()V 6: return LineNumberTable: line 8: 0 line 10: 6}SourceFile: "SynchronizedMethodDemo.java"
上面對代碼塊和方法的實現方式進行探究:
代碼塊通過在編譯后的代碼中添加monitorenter和monitorexit指令。
方法中通過添加ACC_SYNCHRONIZED標志,來決定是否調用monitor對象。
Java 對象頭
synchronized鎖的相關數據存放在 Java 對象頭中。Java 對象頭指的 HotSpot 虛擬機的對象頭,使用2個字寬或3個字寬存儲對象頭。
第一部分存儲運行時的數據,hashCode、鎖標記位、是否偏向鎖、GC分代年齡等等信息,稱作為Mark Word。
第二部分存儲對象類型數據的指針。
第三部分,如果對象是數組的話,則用這部分來存儲數組長度。
Java 對象頭 Mark Word 存儲內容:
鎖升級
synchronized 稱為重量級鎖,但 Java SE 1.6 為優化該鎖的性能而減少獲取和釋放鎖的性能消耗,引入偏向鎖和輕量級鎖。
鎖的高低級別為:無鎖→偏向鎖→輕量級鎖→重量級鎖。
其中鎖的升級是不可逆的,只能由低往高級別升,不能由高往低降。
偏向鎖
偏向鎖是優化在無多線程競爭情況下,提高程序的的運行性能而使用到的鎖。在Mark Word中存儲一個值,用來標志是否為偏向鎖,在 32 位虛擬機和 64 位虛擬機中都是使用一個字節存儲,0 為非偏向鎖,1 為是偏向鎖。
當第一次被線程獲取偏向鎖時,會將Mark Word中的偏向鎖標志設置為 1,同時使用 CAS 操作來記錄這個線程的ID。獲取到偏向鎖的線程,再次進入獲取鎖時,只需判斷Mark Word是否存儲著當前線程ID,如果是,則不需再次進行獲取鎖操作,而是直接持有該鎖。
撤銷鎖
如果有其他線程出現,嘗試獲取偏向鎖,讓偏向鎖處于競爭狀態,那么當前偏向鎖就會撤銷。
撤銷偏向鎖時,首先會暫停持有偏向鎖的線程,并將線程ID設為空,然后檢查該線程是否存活:
當暫停線程非存活,則設置對象頭為無鎖狀態。
當暫停線程存活,執行偏向鎖的棧,最后對象頭的保存其他獲取到偏向鎖的線程ID或者轉向無鎖狀態。
當確定代碼一定執行在多線程訪問中時,那么這時的偏向鎖是無法發揮到優勢,如果繼續使用偏向鎖就顯得過于累贅,給系統帶來不必要的性能開銷,此時可以設置 JVM 參數-XX:BiasedLocking=false來關閉偏向鎖。
輕量級鎖
代碼進入同步塊的時候,如果對象頭不是鎖定狀態,JVM 則會在當前線程的棧楨中創建一個鎖記錄的空間,將鎖對象頭的Mark Word復制一份到鎖記錄中,這份復制過來的Mark Word叫做Displaced Mark Word。然后使用 CAS 操作將鎖對象頭中的Mark Word更新為指向鎖記錄的指針。如果更新成功,當前線程則會獲得鎖,如果失敗,JVM 先檢查鎖對象的Mark Word是否指向當前線程,是指向當前線程的話,則當前線程已持有鎖,否則存在多線程競爭,當前線程會通過自旋獲取鎖,這里的自旋可以理解為循環嘗試獲取鎖,所以這過程是消耗 CPU 的過程。當輕量級鎖存在競爭狀態并自旋獲取輕量級鎖失敗時,輕量級鎖就會膨脹為重量級鎖,鎖對象的Mark Word會更新為指向重量級鎖的指針,等待獲取鎖的線程進入阻塞狀態。
解鎖
輕量級鎖解鎖是使用 CAS 操作將鎖記錄替換到Mark Word中,如果替換成功,則表示同步操作已完成。如果失敗,則表示其他競爭線程嘗試過獲取該輕量級鎖,需要在釋放鎖的同時,去喚醒其他被阻塞的線程,被喚醒的線程回去再次去競爭鎖。
總結
通過分析synchronized的使用以及 Java SE 1.6 升級優化鎖后的設計,可以看出其主要是解決是通過多加入兩級相對更輕巧的偏向鎖和輕量級鎖來優化重量級鎖的性能消耗,但是這并不是一定會起到優化作用,主要是解決大多數情況下不存在多線程競爭以及同一線程多次獲取鎖的的優化,這也是根據平時在編碼中多觀察多反思得出的權衡方案。
推薦閱讀:
《volatile 手摸手帶你解析》:
https://ytao.top/2020/03/15/18-volatile/
《Java 線程通信之 wait/notify 機制》:
https://ytao.top/2020/05/12/24-thread-wait-notify/
《Java 多線程中使用 JDK 自帶工具類實現計數器》:
https://ytao.top/2020/05/17/25-thread-count/
《Java 線程基礎,從這篇開始》:
https://ytao.top/2020/04/19/22-thread-base/
-?END -