synchronized--面試的熱點問題了吧,用法大家都知道:可以作用在代碼塊、靜態方法、實例方法,也知道三種用法鎖的對象是啥,底層原理是什么呢?虛擬機是如何實現的呢?
先看一段被synchronized聲明的代碼塊編譯后的字節碼。
public void foo(Object lock) {
synchronized (lock) {
lock.hashCode();
}
}
// 上面的 Java 代碼將編譯為下面的字節碼
public void foo(java.lang.Object);
Code:
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: aload_1
5: invokevirtual java/lang/Object.hashCode:()I
8: pop
9: aload_2
10: monitorexit
11: goto 19
14: astore_3
15: aload_2
16: ***monitorexit***
17: aload_3
18: athrow
19: return
Exception table:
from to target type
4 11 14 any
14 17 14 any
字節碼的每行意思暫且不研究,會發現很醒目的三行命令,第3行的monitorenter,第10行和第16行的monitorexit。當聲明 synchronized 代碼塊時,編譯而成的字節碼將包含 monitorenter 和 monitorexit 指令。這兩種指令均會消耗操作數棧上的一個引用類型的元素(也就是 synchronized 關鍵字括號里的引用),作為所要加鎖解鎖的鎖對象。
聲明方法時的字節碼
public synchronized void foo(Object lock) {
lock.hashCode();
}
// 上面的 Java 代碼將編譯為下面的字節碼
public synchronized void foo(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: invokevirtual java/lang/Object.hashCode:()I
4: pop
5: return
你會看到字節碼中方法的訪問標記包括 ACC_SYNCHRONIZED,這里 monitorenter 和 monitorexit 操作所對應的鎖對象是隱式的。對于實例方法來說,這兩個操作對應的鎖對象是 this;對于靜態方法來說,這兩個操作對應的鎖對象則是所在類的 Class 實例。
講到這里插入一個Jvm中對象的概念:對象的內存布局
對象在內存中的布局可以分為3塊區域:對象頭(header),實例數據(Instance Data)和對齊填充(Padding)。
對象頭包括兩部分信息:
第一部分用于存儲對象自身的運行時數據。如哈希碼,GC分代年齡,鎖狀態標志,線程持有的鎖,偏向線程id,偏向時間戳等??紤]到虛擬機的空間效率,此部分在32位和64位虛擬機中只占32位或者64位的大小。
第二部分是類型指針。即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。(并不是所有的虛擬機實現都保留類型指針,查找對象的元數據信息不一定要通過對象本身)
如果該對象是數組,那么對象頭還會保留一塊數據,用于記錄數組長度。
很多文章都講到synchronized底層是對象頭+monitor實現的,沒錯,那具體是怎么實現的呢?
對象頭的作用就是提供鎖計數器和指向線程的指針的
當執行 monitorenter 時,如果計數器為 0,說明沒有被其他線程所持有。Java 虛擬機會將該鎖對象的持有線程指向當前線程,并且將其計數器加 1;如果計數器的值不為0,先判斷指針指向是否是當前線程,若是則可獲取鎖(可重入鎖),并且計數器加1,若不是則進入阻塞狀態
當執行 monitorexit 時,Java 虛擬機則需將鎖對象的計數器減 1。當計數器減為 0 時,那便代表該鎖已經被釋放掉了。
為什么上面字節碼中出現兩次 monitorexit?那是考慮到異常情況也要釋放鎖。
自JDK1.5之后就對synchronized做了很大的優化,加入了自旋鎖,輕量級鎖,偏向鎖等
重量級鎖
Java 虛擬機中最為基礎的鎖實現。在這種狀態下,Java 虛擬機會阻塞加鎖失敗的線程,并且在目標鎖被釋放的時候,喚醒這些線程。
缺點:Java 線程的阻塞以及喚醒,都是依靠操作系統來完成的,這些操作將涉及系統調用,需要從操作系統的用戶態切換至內核態,其開銷非常之大。
自旋鎖
為了盡量避免昂貴的線程阻塞、喚醒操作,Java 虛擬機會在線程進入阻塞狀態之前,以及被喚醒后競爭不到鎖的情況下,進入自旋狀態,在處理器上空跑并且輪詢鎖是否被釋放。如果此時鎖恰好被釋放了,那么當前線程便無須進入阻塞狀態,而是直接獲得這把鎖。
缺點:很明顯,自旋占用cup資源,在高并發的情況下很影響系統性能。
輕量級鎖
采用 CAS 操作,將鎖對象的標記字段替換為一個指針,指向當前線程棧上的一塊空間,存儲著鎖對象原本的標記字段。它針對的是多個線程在不同時間段申請同一把鎖的情況。
偏向鎖
只會在第一次請求時采用 CAS 操作,在鎖對象的標記字段中記錄下當前線程的地址。在之后的運行過程中,持有該偏向鎖的線程的加鎖操作將直接返回。它針對的是鎖僅會被同一線程持有的情況。
網上很多文章還在說synchronized的性能不如lock,那是以前,現在看來synchronized的性能會更好,當然考慮到synchronized是非公平鎖,無法手動釋放,沒有讀寫分離功能還是要選擇lock。