預備知識
- Java對象(非數組):用來存儲鎖,由對象頭、實例數據、對齊填充數據組成。

-
對象頭:由MarkWord、類型指針組成。32位JVM下的Markword占32位,存儲的數據取決於鎖的狀態。
-
初始是無鎖狀態。

-
在運行期間MarkWord里存儲的數據會隨着鎖狀態的變化而變化

-
Monitor類型對象:重量級鎖狀態下,MarkWord里的指針指向的對象,ObjectMonitor(C++寫的)對Monitor做的實現。
- ObjectMonitor對象主要屬性:
- _count用來記錄當前線程獲取的鎖計數
- _WaitSet存放處於wait狀態的線程
- _EntryList存放處於等待獲取鎖,處於block狀態的線程隊列。
- _owner指向持有ObjectMonitor對象的線程
- ObjectMonitor對象主要屬性:
synchronized
介紹
- 用來修飾方法(靜態方法、實例方法)、代碼塊
- 常說的通過synchronized加鎖就是指競爭獲取對象頭MarkWord重量級鎖狀態下指向的Monitor類型對象(ObjectMonitor),但是JDK1.6之后有了優化。
- 可以保持原子性(加鎖)、保持變量可見性(釋放鎖會將緩存刷新到主存)、不防止指令重排序(比如單例模式DoubleCheck還是會用到volatile防止指令重排序)、
原理
- JDK1.6之前,在進入synchronized修飾的方法或代碼塊之前要先獲取重量鎖(指的是獲取對象頭指針指向的Monitor類型的對象)
- 當修飾的是靜態方法獲取的是類的Class對象對應的Monitor對象
- 當修飾的是實例方法獲取的是該類的實例對象對應的Monitor對象
- 當修飾的是代碼塊需要自己指定
- 用synchronized修飾的代碼塊,編譯階段會在方法執行前后生成monitorenter、monitorexit指令。
- JVM規范對於monitorenter指令描述:
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
- If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
- If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
- If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
- 每一個對象都有一個Monitor對象,線程通過執行monitorenter指令嘗試獲取Monitor對象的擁有權
- 如果擁有當前Monitor對象的線程數為0,則將_count++,當前線程稱為Monitor對象的擁有者。
- 如果當前線程已經擁有了此Monitor對象,則將_count++即可。
- 如果其他線程已經擁有了此Monitor對象,則當前線程阻塞直到Monitor的計數_count==0,然后重新競爭獲取鎖。
- JVM規范對於monitorexit指令描述:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.- 執行monitorexit指令的線程必須是此Monitor對象的擁有者(否則會拋java.lang.IllegalMonitorStateException異常),線程減少Monitor對象的鎖計數,如果鎖計數為0了,則線程不在是Monitor對象的擁有者,其他被這個Monitor對象阻塞的線程可以嘗試獲取Monitor(之前因沒競爭到鎖而阻塞的線程需要被執行monitorexit指令的線程喚醒才能重新競爭鎖)。
獲取重量鎖過程
-
當線程執行到monitorenter指令,會進入ObjectMonitor對象的_EntryList隊列,通過CAS會將_owner指針指向當前線程,同時_count++,
-
當前線程執行monitorexit指令,會釋放持有的Monitor對象,並將_owner置為null同時_count--
-
如果調用wait(),同上,但是會進入_WaitSet隊列,等待被喚醒。(看到沒:wait狀態的線程在喚醒之后,還得需要獲取鎖④,然后執行完畢)

鎖優化
-
原因:因為獲取重量級鎖過程中,比如將_owner指向當前線程調用的函數涉及到了特權指令Mutex Lock導致用戶態線程和內核態線程之間進行切換,切換過程影響效率(比如需要保存當前線程的在CPU寄存器里的緩存,程序計數器的值等,並將新的線程載入到寄存器,更新PC程序計數器...)
-
JDK1.6做了優化,執行monitorenter指令時不會直接獲取重量鎖,而是先嘗試獲取偏向鎖,=> 輕量鎖 => 重量級鎖。
-
偏向鎖相對於輕量級鎖減少了CAS操作的次數,輕量級鎖相對於重量級鎖減少了系統調用。
獲取偏向鎖過程
- 原因:大部分情況下不會存在線程競爭,而且只會有同一個線程進入臨界區,為了減少同一線程獲取鎖帶來的消耗,所以當進入臨界區前不會先去獲取重量鎖,而是先獲取偏向鎖。
- 膨脹成輕量級鎖:偏向鎖主要是為了解決同一個線程進入臨界區,當有超過一個線程競爭偏向鎖,就會膨脹為輕量級鎖。
- 獲取偏向鎖過程:
- 先判斷是否能開啟偏向鎖,如果可以 => 將偏向鎖偏向線程ID用CAS(相對於輕量級鎖獲取和釋放都需要CAS操作費時,偏向鎖只有這一次)修改為當前線程ID。
獲取輕量鎖過程
- 原因:在多個線程都會嘗試進入臨界區的情況下,多個線程只會交替進入臨界區,不會存在鎖競爭,為了減少重量級鎖系統調用造成的消耗。
- 膨脹成重量級鎖:當多個線程同一時間都嘗試獲取鎖,則會膨脹為重量級鎖。
- 獲取輕量級鎖獲取過程:
- 如果當前無鎖並且不可偏向,會嘗試獲取輕量級鎖,將MarkWord拷貝到當前線程的棧幀中的LockRecord,然后通過CAS更新MarkWord內容為指向當前線程LockRecord的指針,
- 和偏向鎖的區別:偏向鎖是同一個線程多次獲取鎖,輕量級鎖是多個線程交替獲取鎖。相同點是假定都不存在鎖競爭。
自旋鎖
- 原因:咳咳,還是大部分情況下,線程持有鎖的時間很短,當一個線程獲取鎖了以后,其他線程嘗試獲取鎖就會進入阻塞狀態,掛起->恢復都需要在用戶態和內核態之間進行切換。此時如果讓后來的線程進行自旋一段時間(for循環),在獲取鎖,可能就會獲取,也就避免了轉入內核態。
- JDK1.6引入了自適應的自旋鎖,即根據具體情況結合前面旋轉的次數決定此次需要旋轉的次數。
- 優點:如果線程占用鎖的時間比較短則自旋操作很有效,避免進入內核態
- 缺點:如果線程占用鎖的時間比較長則自旋操作白白耗費CPU資源,倒不如掛起。
其他
- 當修飾方法會在常量池生成ACC_SYNCHRONIZED,當執行某個方法時會如果遇到此指令會同上...
public class Test {
synchronized void test() {
synchronized (this.getClass()) {}
}
}

