synchronized
是 Java 編程中的一個重要的關鍵字,也是多線程編程中不可或缺的一員。本文就對它的使用和鎖的一些重要概念進行分析。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
中。spa
先將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."<init>":()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_SUPER
Constant pool:
#1 = Methodref #5.#13 // java/lang/Object."<init>":()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 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 demo
#11 = Utf8 SourceFile
#12 = Utf8 SynchronizedMethodDemo.java
#13 = NameAndType #6:#7 // "<init>":()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."<init>":()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
對象。synchronized
鎖的相關數據存放在 Java 對象頭中。Java 對象頭指的 HotSpot 虛擬機的對象頭,使用2個字寬或3個字寬存儲對象頭。
Mark Word
。Java 對象頭 Mark Word 存儲內容:
存儲內容 | 標誌位 | 狀態 |
---|---|---|
對象的hashCode、GC分代年齡 | 01 | 無鎖 |
指向棧中鎖記錄的指針 | 00 | 輕量級鎖 |
指向重量級鎖的指針 | 10 | 重量級鎖 |
空 | 11 | GC標記 |
線程ID、Epoch(一個時間戳)、GC分代年齡 | 01 | 偏向鎖 |
synchronized 稱爲重量級鎖,但 Java SE 1.6 爲優化該鎖的性能而減小獲取和釋放鎖的性能消耗,引入偏向鎖
和輕量級鎖
。
鎖的高低級別爲:無鎖
→偏向鎖
→輕量級鎖
→重量級鎖
。
其中鎖的升級是不可逆的,只能由低往高級別升,不能由高往低降。
偏向鎖是優化在無多線程競爭狀況下,提升程序的的運行性能而使用到的鎖。在Mark Word
中存儲一個值,用來標誌是否爲偏向鎖,在 32 位虛擬機和 64 位虛擬機中都是使用一個字節存儲,0 爲非偏向鎖,1 爲是偏向鎖。
當第一次被線程獲取偏向鎖時,會將Mark Word
中的偏向鎖標誌設置爲 1,同時使用 CAS 操做來記錄這個線程的ID。獲取到偏向鎖的線程,再次進入獲取鎖時,只需判斷Mark Word
是否存儲着當前線程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 升級優化鎖後的設計,能夠看出其主要是解決是經過多加入兩級相對更輕巧的偏向鎖和輕量級鎖來優化重量級鎖的性能消耗,可是這並非必定會起到優化做用,主要是解決大多數狀況下不存在多線程競爭以及同一線程屢次獲取鎖的的優化,這也是根據平時在編碼中多觀察多反思得出的權衡方案。