1. 公平鎖 vs 非公平鎖
公平鎖:是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。類似排隊打飯,先來后到。
非公平鎖:是指多個線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優先獲取鎖。
比較
公平鎖,就是很公平,在并發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果為空,或者當前線程是等待隊列的第一個,就占有鎖,否則就會加入到等待隊列中,以后會按照 FIFO 的規則從隊列中取到自己。
非公平鎖,比較粗魯,上來就直接嘗試占有鎖,如果嘗試失敗,就再采用類似公平鎖的方式。
公平鎖的優點是等待鎖的線程不會餓死。
非公平鎖的優點在于吞吐量比公平鎖大。但在高并發的情況下,有可能會造成優先級反轉或饑餓現象。
內窺
并發包中 ReentrantLock 的創建可以指定構造函數的 boolean 類型來得到公平鎖或非公平鎖,默認為非公平鎖。
查看 ReentrantLock,可以看到有一個繼承自
AbstractQueuedSynchronizer 的內部類 Sync,添加鎖和釋放鎖的大部分操作實際上都是在 Sync 中實現的。它有公平鎖 FairSync 和非公平鎖 NonfairSync 兩個子類。
public class ReentrantLock implements Lock, JAVA.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
//......
}
static final class NonfairSync extends Sync {
//......
}
static final class FairSync extends Sync {
//......
}
}
兩個構造方法對比,可以看出公平鎖和非公平鎖的區別
- 非公平鎖在調用 lock() 后,首先就會通過 CAS 進行一次搶鎖,如果這個時候恰巧鎖沒有被占用,那么直接就獲取到鎖返回了,否則按公平鎖的方式去排隊,進入到阻塞隊列等待喚醒
- 公平鎖在獲取同步狀態(獲取鎖)時 tryAcquire() 多了一個限制條件:!hasQueuedPredecessors() ,用來判斷當前線程是否位于同步隊列中的第一個
Synchronized關鍵字,也是一種非公平鎖。
2. 樂觀鎖 VS 悲觀鎖
樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待線程同步的不同角度。在 Java 和數據庫中都有此概念對應的實際應用。
- 悲觀鎖是一種悲觀思想,它總認為自己在使用數據的時候一定有別的線程來修改,所以悲觀鎖在持有數據的時候總會把資源或數據鎖住,這樣其他線程想要請求這個資源的時候就會阻塞,直到等到悲觀鎖把資源釋放為止。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。悲觀鎖的實現往往依靠數據庫本身的鎖功能實現。
- Java 中,synchronized 關鍵字和 Lock 的實現類都是悲觀鎖。
- 而樂觀鎖認為自己在使用數據時不會有別的線程修改數據,所以不會添加鎖,只是在更新數據的時候去判斷之前有沒有別的線程更新了這個數據。如果這個數據沒有被更新,當前線程將自己修改的數據成功寫入。如果數據已經被其他線程更新,則根據不同的實現方式執行不同的操作(例如報錯或者自動重試)
- 樂觀鎖的實現方案一般來說有兩種: 版本號機制 和 CAS實現 。
- Java 中 java.util.concurrent.atomic 包下面的原子變量類的遞增操作就是通過 CAS 實現了樂觀鎖。
比較
悲觀鎖:比較適合寫入操作比較頻繁的場景,如果出現大量的讀取操作,每次讀取的時候都會進行加鎖,這樣會增加大量的鎖的開銷,降低了系統的吞吐量。
樂觀鎖:比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,數據發生沖突的可能性就會增大,為了保證數據的一致性,應用層需要不斷的重新獲取數據,這樣會增加大量的查詢操作,降低了系統的吞吐量。
悲觀鎖比較適合強一致性的場景,但效率比較低,特別是讀得并發低。樂觀鎖則適用于讀多寫少,并發沖突少的場景。
樂觀鎖常見的問題:
- ABA 問題
- 循環時間長開銷大
- 只能保證一個共享變量的原子操作
3. 可重入鎖(遞歸鎖)
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因為之前已經獲取過還沒釋放而阻塞。
也就是說,線程可以進入任何一個它已經擁有的鎖同步著的代碼塊。
可重入鎖的最大作用是可一定程度避免死鎖,ReentrackLock、Synchronized 就是典型的可重入鎖。
public class Wget {
public synchronized void doSomething() {
System.out.println("方法1執行...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("方法2執行...");
}
}
在上面的代碼中,類中的兩個方法都是被內置鎖 synchronized 修飾的,doSomething() 方法中調用 doOthers() 方法。因為內置鎖是可重入的,所以同一個線程在調用 doOthers() 時可以直接獲得當前對象的鎖,進入doOthers() 進行操作。
如果是一個不可重入鎖,那么當前線程在調用 doOthers() 之前需要將執行 doSomething() 時獲取當前對象的鎖釋放掉,實際上該對象鎖已被當前線程所持有,且無法釋放。所以此時會出現死鎖。
自旋鎖
自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗 CPU
public class SpinLockDemo {
AtomicReference<Thread> lock = new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread();
//如果不為空,自旋
while (!lock.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread = Thread.currentThread();
//解鎖后,將鎖置為 null
lock.compareAndSet(thread,null);
}
}
優缺點
缺點:
- 如果某個線程持有鎖的時間過長,就會導致其它等待獲取鎖的線程進入循環等待,消耗CPU。使用不當會造成CPU 使用率極高。
- 上面 Java 實現的自旋鎖不是公平的,即無法滿足等待時間最長的線程優先獲取鎖。不公平的鎖就會存在“線程饑餓”問題。
優點:
- 自旋鎖不會使線程狀態發生切換,一直處于用戶態,即線程一直都是 active 的;不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快
- 非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候需要從內核態恢復,需要線程上下文切換。 (線程被阻塞后便進入內核(linux)調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能)
可重入的自旋鎖和不可重入的自旋鎖
上邊寫的自旋鎖,仔細分析一下可以看出,它是不支持重入的,即當一個線程第一次已經獲取到了該鎖,在鎖釋放之前又一次重新獲取該鎖,第二次就不能成功獲取到。由于不滿足 CAS,所以第二次獲取會進入 while 循環等待,而如果是可重入鎖,第二次也是應該能夠成功獲取到的。
而且,即使第二次能夠成功獲取,那么當第一次釋放鎖的時候,第二次獲取到的鎖也會被釋放,而這是不合理的。
為了實現可重入鎖,我們需要引入一個計數器,用來記錄獲取鎖的線程數。
自旋鎖與互斥鎖
- 自旋鎖與互斥鎖都是為了實現保護資源共享的機制。
- 無論是自旋鎖還是互斥鎖,在任意時刻,都最多只能有一個保持者。
- 獲取互斥鎖的線程,如果鎖已經被占用,則該線程將進入睡眠狀態;獲取自旋鎖的線程則不會睡眠,而是一直循環等待鎖釋放。
總結:
- 自旋鎖:線程獲取鎖的時候,如果鎖被其他線程持有,則當前線程將循環等待,直到獲取到鎖。
- 自旋鎖等待期間,線程的狀態不會改變,線程一直是用戶態并且是活動的(active)。
- 自旋鎖如果持有鎖的時間太長,則會導致其它等待獲取鎖的線程耗盡CPU。
- 自旋鎖本身無法保證公平性,同時也無法保證可重入性。
- 基于自旋鎖,可以實現具備公平性和可重入性質的鎖。
4. 獨占鎖(互斥鎖/寫鎖)、共享鎖(讀鎖)
獨占鎖:指該鎖一次只能被一個線程所持有,對 ReentrantLock 和 Synchronized 而言都是獨占鎖
共享鎖:指該鎖可被多個線程所持有
對 ReentrantReadWriteLock 其讀鎖是共享鎖,其寫鎖是獨占鎖。
讀鎖的共享鎖可保證并發讀是非常高效的,讀寫、寫讀、寫寫的過程是互斥的。
無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖
這四種鎖是指鎖的狀態,專門針對 synchronized 的。在介紹這四種鎖狀態之前還需要介紹一些額外的知識。
首先為什么 Synchronized 能實現線程同步?
在回答這個問題之前我們需要了解兩個重要的概念:“Java對象頭”、“Monitor”。
無鎖
無鎖沒有對資源進行鎖定,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功。
無鎖的特點就是修改操作在循環內進行,線程會不斷的嘗試修改共享資源。如果沒有沖突就修改成功并退出,否則就會繼續循環嘗試。如果有多個線程修改同一個值,必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功。上面我們介紹的CAS原理及應用即是無鎖的實現。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的。
偏向鎖
偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖,降低獲取鎖的代價。
在大多數情況下,鎖總是由同一線程多次獲得,不存在多線程競爭,所以出現了偏向鎖。其目標就是在只有一個線程執行同步代碼塊時能夠提高性能。
當一個線程訪問同步代碼塊并獲取鎖時,會在Mark word里存儲鎖偏向的線程ID。在線程進入和退出同步塊時不再通過CAS操作來加鎖和解鎖,而是檢測Mark Word里是否存儲著指向當前線程的偏向鎖。引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令即可。
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態。撤銷偏向鎖后恢復到無鎖(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態。
偏向鎖在JDK 6及以后的JVM里是默認啟用的。可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之后程序默認會進入輕量級鎖狀態。
輕量級鎖
是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。
在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(鎖標志位為“01”狀態,是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,然后拷貝對象頭中的Mark Word復制到鎖記錄中。
拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向對象的Mark Word。
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設置為“00”,表示此對象處于輕量級鎖定狀態。
如果輕量級鎖的更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明多個線程競爭鎖。
若當前只有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖。
重量級鎖
升級為重量級鎖時,鎖標志的狀態值變為“10”,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態。
綜上,偏向鎖通過對比Mark Word解決加鎖問題,避免執行CAS操作。而輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。重量級鎖是將除了擁有鎖的線程以外的線程都阻塞。