synchronized 原理知多少
synchronized是 Java 編程中的一個重要的關(guān)鍵字,也是多線程編程中不可或缺的一員。本文就對它的使用和鎖的一些重要概念進行分析。
使用及原理
synchronized 是一個重量級鎖,它主要實現(xiàn)同步操作,在 Java 對象鎖中有三種使用方式:
普通方法中使用,鎖是當前實例對象。 靜態(tài)方法中使用,鎖當前類的對象。 代碼塊中使用,鎖是代碼代碼塊中配置的對象。
使用
在代碼中使用方法分別如下:
普通方法使用:
/**
* 公眾號:ytao
* 博客:https://ytao.top
*/
public class SynchronizedMethodDemo{
public synchronized void demo(){
// ......
}
}
靜態(tài)方法使用:
/**
* 公眾號: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){
// ......
}
}
}
實現(xiàn)原理
方法和代碼塊的實現(xiàn)原理使用不同方式:
代碼塊
每個對象都擁有一個monitor對象,代碼塊的{}中會插入monitorenter和monitorexit指令。當執(zhí)行monitorenter指令時,會進入monitor對象獲取鎖,當執(zhí)行monitorexit命令時,會退出monitor對象釋放鎖。同一時刻,只能有一個線程進入在monitorenter中。
先將SynchronizedDemo.java使用javac SynchronizedDemo.java命令將其編譯成SynchronizedDemo.class。然后使用javap -c SynchronizedDemo.class反編譯字節(jié)碼。
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指令,一個插入在異常位置,一個插入在方法結(jié)束位置。
方法
方法中的synchronized與代碼塊中實現(xiàn)的方式不同,方法中會添加一個叫ACC_SYNCHRONIZED的標志,當調(diào)用方法時,首先會檢查是否有ACC_SYNCHRONIZED標志,如果存在,則獲取monitor對象,調(diào)用monitorenter和monitorexit指令。
通過javap -v -c SynchronizedMethodDemo.class命令反編譯SynchronizedMethodDemo類。-v參數(shù)即-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"
上面對代碼塊和方法的實現(xiàn)方式進行探究:
代碼塊通過在編譯后的代碼中添加 monitorenter和monitorexit指令。方法中通過添加 ACC_SYNCHRONIZED標志,來決定是否調(diào)用monitor對象。
Java 對象頭
synchronized鎖的相關(guān)數(shù)據(jù)存放在 Java 對象頭中。Java 對象頭指的 HotSpot 虛擬機的對象頭,使用2個字寬或3個字寬存儲對象頭。
第一部分存儲運行時的數(shù)據(jù),hashCode、鎖標記位、是否偏向鎖、GC分代年齡等等信息,稱作為 Mark Word。第二部分存儲對象類型數(shù)據(jù)的指針。 第三部分,如果對象是數(shù)組的話,則用這部分來存儲數(shù)組長度。
Java 對象頭 Mark Word 存儲內(nèi)容:
| 存儲內(nèi)容 | 標志位 | 狀態(tài) |
|---|---|---|
| 對象的hashCode、GC分代年齡 | 01 | 無鎖 |
| 指向棧中鎖記錄的指針 | 00 | 輕量級鎖 |
| 指向重量級鎖的指針 | 10 | 重量級鎖 |
| 空 | 11 | GC標記 |
| 線程ID、Epoch(一個時間戳)、GC分代年齡 | 01 | 偏向鎖 |
鎖升級
synchronized 稱為重量級鎖,但 Java SE 1.6 為優(yōu)化該鎖的性能而減少獲取和釋放鎖的性能消耗,引入偏向鎖和輕量級鎖。
鎖的高低級別為:無鎖→偏向鎖→輕量級鎖→重量級鎖。
其中鎖的升級是不可逆的,只能由低往高級別升,不能由高往低降。
偏向鎖
偏向鎖是優(yōu)化在無多線程競爭情況下,提高程序的的運行性能而使用到的鎖。在Mark Word中存儲一個值,用來標志是否為偏向鎖,在 32 位虛擬機和 64 位虛擬機中都是使用一個字節(jié)存儲,0 為非偏向鎖,1 為是偏向鎖。
當?shù)谝淮伪痪€程獲取偏向鎖時,會將Mark Word中的偏向鎖標志設(shè)置為 1,同時使用 CAS 操作來記錄這個線程的ID。獲取到偏向鎖的線程,再次進入獲取鎖時,只需判斷Mark Word是否存儲著當前線程ID,如果是,則不需再次進行獲取鎖操作,而是直接持有該鎖。
撤銷鎖
如果有其他線程出現(xiàn),嘗試獲取偏向鎖,讓偏向鎖處于競爭狀態(tài),那么當前偏向鎖就會撤銷。撤銷偏向鎖時,首先會暫停持有偏向鎖的線程,并將線程ID設(shè)為空,然后檢查該線程是否存活:
當暫停線程非存活,則設(shè)置對象頭為無鎖狀態(tài)。 當暫停線程存活,執(zhí)行偏向鎖的棧,最后對象頭的保存其他獲取到偏向鎖的線程ID或者轉(zhuǎn)向無鎖狀態(tài)。
當確定代碼一定執(zhí)行在多線程訪問中時,那么這時的偏向鎖是無法發(fā)揮到優(yōu)勢,如果繼續(xù)使用偏向鎖就顯得過于累贅,給系統(tǒng)帶來不必要的性能開銷,此時可以設(shè)置 JVM 參數(shù)-XX:BiasedLocking=false來關(guān)閉偏向鎖。
輕量級鎖
代碼進入同步塊的時候,如果對象頭不是鎖定狀態(tài),JVM 則會在當前線程的棧楨中創(chuàng)建一個鎖記錄的空間,將鎖對象頭的Mark Word復(fù)制一份到鎖記錄中,這份復(fù)制過來的Mark Word叫做Displaced Mark Word。然后使用 CAS 操作將鎖對象頭中的Mark Word更新為指向鎖記錄的指針。如果更新成功,當前線程則會獲得鎖,如果失敗,JVM 先檢查鎖對象的Mark Word是否指向當前線程,是指向當前線程的話,則當前線程已持有鎖,否則存在多線程競爭,當前線程會通過自旋獲取鎖,這里的自旋可以理解為循環(huán)嘗試獲取鎖,所以這過程是消耗 CPU 的過程。當輕量級鎖存在競爭狀態(tài)并自旋獲取輕量級鎖失敗時,輕量級鎖就會膨脹為重量級鎖,鎖對象的Mark Word會更新為指向重量級鎖的指針,等待獲取鎖的線程進入阻塞狀態(tài)。
解鎖
輕量級鎖解鎖是使用 CAS 操作將鎖記錄替換到Mark Word中,如果替換成功,則表示同步操作已完成。如果失敗,則表示其他競爭線程嘗試過獲取該輕量級鎖,需要在釋放鎖的同時,去喚醒其他被阻塞的線程,被喚醒的線程回去再次去競爭鎖。
總結(jié)
通過分析
synchronized的使用以及 Java SE 1.6 升級優(yōu)化鎖后的設(shè)計,可以看出其主要是解決是通過多加入兩級相對更輕巧的偏向鎖和輕量級鎖來優(yōu)化重量級鎖的性能消耗,但是這并不是一定會起到優(yōu)化作用,主要是解決大多數(shù)情況下不存在多線程競爭以及同一線程多次獲取鎖的的優(yōu)化,這也是根據(jù)平時在編碼中多觀察多反思得出的權(quán)衡方案。
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取