synchronized是什麼
synchronized是java同步鎖,同一時刻多個執行緒對同一資源進行修改時,能夠保證同一時刻只有一個執行緒獲取到資源並對其進行修改,因此保證了執行緒安全性。
synchronized可以修飾方法和程式碼塊,底層實現的邏輯略有不同。
Object obj=new Object();
synchronized(obj){
//do soming
}
編譯後的程式碼為:
...
10 astore_2
11 monitorenter
12 aload_2
13 monitorexit
14 goto 22 (+8)
17 astore_3
18 aload_2
19 monitorexit
20 aload_3
21 athrow
22 return
當程式碼執行到synchronize(obj)
時,對應的位元組碼為monitorenter
進行加鎖操作,程式碼執行完後就是monitorexit
進行鎖的釋放。兩個 monitorexit
是正常退出和異常退出兩種情況下鎖的釋放。
public synchronized void test1(){
//do somthing
}
當修飾方法時是在編譯後的位元組碼上加上了synchronized
的訪問標識
Monitor機制
Monitor是一種同步機制,它的作用是保證同一時刻只有一個執行緒能訪問到受保護的資源,JVM中的同步是基於進入和退出監視物件來實現的,是synchronized
的底層實現,每個物件例項都是一個Montor物件,Monitor對應的是底層的MonitorObject,是基於作業系統的互斥mutex
實現的。
ObjectMonitor中有幾個關鍵屬性
屬性 | 描述 |
---|---|
_owner | 指向持有ObjectMonitor物件的執行緒 |
_WaitSet | 存放處於wait狀態的執行緒佇列 |
_EntryList | 存放處於等待鎖block狀態的執行緒佇列 |
_recursions | 鎖的重入次數 |
_count | 用來記錄該執行緒獲取鎖的次數 |
- 進入monitor,被分配到
Entry List
中,等待持有鎖的執行緒釋放鎖, - 當執行緒獲取到鎖後,是鎖的持有者,
owner
指向當前執行緒 - 當執行緒進行
wait
時進入Wait Set
,等待鎖的持有者進行喚醒。
synchronized鎖的實現原理
- 當程式碼執行到被
synchronized
修飾的程式碼塊或方法時,首先通過monitor
去獲取物件例項的鎖 - 當獲取到鎖時,會在物件例項的
物件頭
上新增鎖標識位 - 沒有獲取到鎖的執行緒,會進行到對物件例項的
entry list
中進行等待 - 持有鎖的執行緒的業務處理完後通過修改
物件頭
上鎖標識位來進行釋放鎖 - 當執行緒進行
wait
操作時,當前也會釋放鎖,然後進行wait set
區等待被喚醒 - 在
entry list
中處理等待的執行緒再次進行鎖的競爭
Mark Word
一個物件的建立要經過這幾步:
- 載入:如果物件的Class還沒載入
- 連結:由符號引用轉換為地址引用
- 初始化:執行Class的
方法 - 開闢一個地址空間(可以使用TLAB技術進行優化,避免通過CAS產生的資源競爭)
- 初始化物件頭資訊
- 執行程式碼的
方法
7.返回物件地址
一個物件有:物件頭
、例項資料
和對齊填充
三部分組成
物件頭有:物件標記(Mark Word)
和型別指標
組成,如果物件是陣列,物件頭中還有陣列的長度
在64位系統中,物件標記佔8個位元組,型別指標佔8個位元組,物件頭共點16個位元組
物件標記中有hashcode碼
、GC年齡
、鎖標記
組成
每個位元組佔8位,8個位元組的MarkWord共佔64位
在無鎖
的狀態下,前25位沒有使用,緊接著的32位儲存了物件的hashcode
,在1位未使用,後面的4位物件的GC年齡
,後面的3位是鎖標記位。
為什麼GC年齡不能超過16
在MarkWord中可以看出GC年齡標記只有4位,二進位制表示就是:1111
,對應的十進位制就是15。
下面通過jol
進行檢視MarkWord的資訊,
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
無鎖時
import org.openjdk.jol.info.ClassLayout;
public class MarkWordTest {
public static void main(String[] args) {
Hummy hummy=new Hummy();
int hashCode = hummy.hashCode();
System.out.println(hashCode);
System.out.println("二進位制:"+Integer.toBinaryString(hashCode));
System.out.println("十六進位制: "+Integer.toHexString(hashCode));
System.out.println(ClassLayout.parseInstance(hummy).toPrintable());
}
}
class Hummy{}
列印出的結果如下:
可以看到物件的hashcode是:6f496d9f
,可以在左邊的Value的找到hashcode值,只不過是反過來的。
最後1位元組的00000001
包含了gc年齡和鎖標記位。
加鎖時
import org.openjdk.jol.info.ClassLayout;
public class MarkWordTest {
public static void main(String[] args) {
//java -XX:BiasedLockingStartupDelay=0
Hummy hummy=new Hummy();
synchronized (hummy){
System.out.println(ClassLayout.parseInstance(hummy).toPrintable());
}
}
}
class Hummy{}
最後一個00000101
的最後3位101
表示偏向鎖
synchronized的優化
jdk1.6之前只有重量級鎖,面在java1.6之後對synchronized的鎖進行了優化,有偏向鎖、輕量級鎖、重量級鎖,主要是因為重量級鎖需要用到作業系統mutex
,作業系統實現執行緒之間的切換需要從使用者態到核心態的,成本非常高。
鎖 | 鎖標識 | 場景 |
---|---|---|
無鎖 | 001 | 不受保護時 |
偏向鎖 | 101 | 只有一個線競爭時 |
輕量級鎖 | 00 | 競爭不激烈時 |
重量級鎖 | 10 | 競爭非常激烈 |
鎖升級的過程:
- 當訪問同步程式碼時,首先判斷markword是否是
無鎖狀態(001)
或者在偏向鎖狀態下markword中的執行緒id與當前執行緒id是否一樣,如果是則把當前執行緒id通過CAS的方式設定到markword中 - 設定成功後則鎖標記修改為(101),升級為偏向當前執行緒的
編向鎖(101)
,執行同步內的方法 - 如果失敗,則由jvm進行偏向鎖的撤消
- 當持有鎖的執行緒執行到
安全點
時,檢查偏向鎖的狀態 - 當持有鎖的執行緒
已退出同步方法
時,釋放原執行緒持有的鎖,變成無鎖狀態,到1處執行 - 當持有鎖的執行緒
還在同步程式碼
中,則升級鎖為輕量級鎖(00)
,當前執行緒持有,另個執行緒通過CAS的方法進行獲取鎖,當自旋到一定次數(20)時,則升級為重量級鎖(10)
,進入堵塞狀態。