Synchronized關鍵字的底層原理?
1. synchronized的基本使用
在現實場景中,搶票代碼,如果不加鎖,就會出現超賣或者一張票賣給多個人
Synchronized對象鎖采用互斥的方式讓同一時刻至多只有一個線程能持有對象鎖,其它線程再想獲取這個對象鎖時就會阻塞住,代碼如下
public class synchronizedTest {
// 創建一個靜態對象作為鎖
static Object lock = new Object();
// 初始票數
int ticketNum = 20;
// 獲取票的方法,使用 synchronized 修飾確保線程安全
public synchronized void getTicket() {
// 使用當前對象作為鎖
synchronized (this) {
// 如果票數已經為零,則返回
if (ticketNum <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "搶到一張票,剩余:" + ticketNum);
// 非原子性操作,扣除一張票
ticketNum--;
}
}
public static void main(String[] args) {
// 創建 synchronizedTest 實例
synchronizedTest synchronizedTest = new synchronizedTest();
// 創建并啟動 20 個線程
for (int i = 0; i < 20; i++) {
// 調用獲取票的方法
new Thread(() -> synchronizedTest.getTicket()).start();
}
}
}
通過以上代碼,加synchronized鎖,就可以防止超賣
特別說明:synchronized 關鍵字的底層實現涉及到 Java 虛擬機中的監視器(Monitor)機制。每個 Java 對象都與一個 Monitor 相關聯,Monitor 負責對象的鎖定和解鎖,以及線程的阻塞和喚醒。
2. Monitor
Monitor 被翻譯為監視器,是由jvm提供,c++語言實現
使用一下簡單代碼中查看monitor,通過javap命令查看clsss的字節碼
public class MonitorTest {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
圖片
- monitorenter: 上鎖開始的地方
- monitorexit: 解鎖的地方
- 其中被monitorenter和monitorexit包圍住的指令就是上鎖的代碼
思考:為什么會出現兩個monitorexit
有兩個monitorexit的原因,第二個monitorexit是為了防止鎖住的代碼拋異常后不能及時釋放鎖在使用了synchornized代碼塊時需要指定一個對象,所以synchornized也被稱為對象鎖
monitor主要就是跟這個對象產生關聯,如下圖
圖片
Monitor內部具體的存儲結構:
- Owner:存儲當前獲取鎖的線程的,只能有一個線程可以獲取
- EntryList:關聯沒有搶到鎖的線程,處于Blocked狀態的線程
- WaitSet:關聯調用了wait方法的線程,處于Waiting狀態的線程
具體的流程:
- 代碼進入synchorized代碼塊,先讓lock(對象鎖)關聯的monitor,然后判斷Owner是否有線程持有
- 如果沒有線程持有,則讓當前線程持有,表示該線程獲取鎖成功
- 如果有線程持有,則讓當前線程進入entryList進行阻塞,如果Owner持有的線程已經釋放了鎖,在EntryList中的線程去競爭鎖的持有權(非公平)
- 如果代碼塊中調用了wait()方法,則會進去WaitSet中進行等待
3.面試題
面試官:synchronized關鍵字的底層原理?
- Synchronized【對象鎖】
- 采用互斥的方式讓同一時刻至多只有一個線程能持有【對象鎖】
- 它的底層由monitor實現的,monitor是jvm級別的對象( C++實現),線程獲得鎖需要使用對象(鎖)關聯monitor
- 在monitor內部有三個屬性,分別是owner、entrylist、waitset
- 其中owner是關聯的獲得鎖的線程,并且只能關聯一個線程;entrylist關聯的是處于阻塞狀態的線程;waitset關聯的是處于Waiting狀態的線程