四、LOCK
1、鎖的4種狀態
【1】鎖的優化
JDK6爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”
從而使得鎖有了4種狀態,並隨着鎖競爭的情況而升級。鎖可以升級但不能降級,鎖的升級被稱爲“鎖膨脹”
【2】偏向鎖狀態
大多數情況下,鎖不僅不存在多線程競爭,而且還總是由同一個線程多次獲得
爲了提升這種情況的性能,從而引入偏向鎖
在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,當一個線程進入和退出同步塊時,不需要進行CAS操作來加鎖和解鎖,直接判斷對象頭的Mark Word裏是否存有指向當前線程的偏向鎖即可
如果存在表示線程已經獲取了鎖,如果不存在則鎖膨脹成輕量級鎖
偏向鎖在JDK6和JDK7中是默認開啓的,但是需要在程序啓動幾秒鐘後纔會被激活
如果需要關閉這個延遲,使用JVM參數
-XX:BiasedLockingStartupDelay=0
如果關閉偏向鎖,使用JVM參數
-XX:-UseBiasedLocking
【3】輕量級鎖狀態
加鎖:JVM在當前線程的棧幀中創建用於存儲鎖標記的空間,並將對象頭中的Displaced Mark Word複製到當中。如果成功表示當前線程獲取鎖成功,如果失敗表示存在鎖競爭,當前線程會嘗試使用自旋來獲取鎖
解鎖:使用原子的CAS操作,將Displaced Mark Word替換回對象頭。如果成功表示沒有鎖競爭發生,如果失敗表示有鎖競爭發生,會導致鎖膨脹成重量級鎖
【4】對比
鎖類型 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外等待, 和無鎖狀態執行方法相比存在納秒級(1納秒 = 1000 * 1000毫秒)的差別 |
如果存在鎖競爭, 會帶來額外的撤銷鎖的消耗 |
只有一個線程訪問同步塊 |
輕量級鎖 | 競爭的線程不會阻塞, 提高了程序的響應速度 |
如果線程一直獲取不到鎖, 使用自旋會消耗CPU |
追求響應速度, 同步塊執行速度非常快 |
重量級鎖 | 線程競爭不會使用自旋,不會消耗CPU | 競爭的線程會阻塞, 會降低響應速度 |
追求吞吐量,同步塊執行時間較長 |
2、公平鎖和非公平鎖
公平鎖:每個線程在獲取鎖時,會先檢查該鎖維護的等待隊列,如果隊列爲空,或者當前線程是隊列當中的第一個(head),則該線程獲取鎖。以後會按照FIFO的原則從等待隊列中取到自己(排隊等待獲取鎖)
非公平鎖:每個線程都可以嘗試獲取鎖
synchronized是非公平鎖
java.util.concurrent.locks.ReentrantLock,通過無參構造函數創建的是非公平鎖。通過有參構造函數,且傳入true,則該鎖是一個公平鎖
3、可重入鎖
可重入鎖又稱之爲遞歸鎖,指的是已經獲取鎖的線程,當執行同步方法內的同步方法時,無需再次獲取鎖,即可直接執行方法
synchronized 和 java.util.concurrent.locks.ReentrantLock 都是可重入鎖
4、自旋鎖
線程通過自旋的方式,嘗試獲取鎖,以減少線程上下文切換的開銷
class CasLock {
private final AtomicReference<Thread> reference = new AtomicReference<Thread>();
public void lock() {
for (;;) {
if (tryAcquire()) {
break;
}
}
}
public void unlock() {
for (;;) {
if (tryRelease()) {
break;
}
}
}
private boolean tryAcquire() {
Thread thread = Thread.currentThread();
return reference.compareAndSet(null, thread);
}
private boolean tryRelease() {
Thread thread = Thread.currentThread();
return reference.compareAndSet(thread, null);
}
}
5、共享鎖和排他鎖
共享鎖:多個線程都可以獲取該鎖
排他鎖:鎖只能由一個線程所獨享
java.util.concurrent.locks.ReentrantReadWriteLock的讀鎖是線程共享的,而寫鎖是線程獨享的
synchronized 和 java.util.concurrent.locks.ReentrantLock 都是排他鎖
6、讀寫鎖
java.util.concurrent.locks.ReentrantReadWriteLock
讀寫鎖,讀鎖和寫鎖共同存在,讀鎖共享,寫鎖獨享
7、線程兼容和線程對立
線程兼容:要操作的對象本身是線程不安全的,但是可以在操作對象時,通過各種線程安全的手段,來保證在多線程的環境下,操作對象是線程安全的
線程對立:要操作的對象本身是線程安全的,但是由於在多線程的環境下操作對象時,多個線程的併發請求不是線程安全的,從而導致操作對象本身變成了線程不安全的
Java中線程對立的例子:Thread類的廢棄方法(@Deprecated) suspend
和 resume
可能導致死鎖