[轉]自旋鎖、排隊自旋鎖、MCS鎖、CLH鎖
自旋鎖(Spin lock)
自旋鎖是指當一個執行緒嘗試獲取某個鎖時,如果該鎖已被其他執行緒佔用,就一直迴圈檢測鎖是否被釋放,而不是進入執行緒掛起或睡眠狀態。
自旋鎖適用於鎖保護的臨界區很小的情況,臨界區很小的話,鎖佔用的時間就很短。
簡單的實現
import java.util.concurrent.atomic.AtomicReference; public class SpinLock { private AtomicReference<Thread> owner = new AtomicReference<Thread>(); public void lock() { Thread currentThread = Thread.currentThread(); // 如果鎖未被佔用,則設定當前執行緒為鎖的擁有者 while (owner.compareAndSet(null, currentThread)) { } } public void unlock() { Thread currentThread = Thread.currentThread(); // 只有鎖的擁有者才能釋放鎖 owner.compareAndSet(currentThread, null); } }
SimpleSpinLock裡有一個owner屬性持有鎖當前擁有者的執行緒的引用,如果該引用為null,則表示鎖未被佔用,不為null則被佔用。
這裡用AtomicReference是為了使用它的原子性的compareAndSet方法(CAS操作),解決了多執行緒併發操作導致資料不一致的問題,確保其他執行緒可以看到鎖的真實狀態。
缺點
- CAS操作需要硬體的配合;
- 保證各個CPU的快取(L1、L2、L3、跨CPU Socket、主存)的資料一致性,通訊開銷很大,在多處理器系統上更嚴重;
- 沒法保證公平性,不保證等待程序/執行緒按照FIFO順序獲得鎖。
Ticket Lock
Ticket Lock 是為了解決上面的公平性問題,類似於現實中銀行櫃檯的排隊叫號:鎖擁有一個服務號,表示正在服務的執行緒,還有一個排隊號;每個執行緒嘗試獲取鎖之前先拿一個排隊號,然後不斷輪詢鎖的當前服務號是否是自己的排隊號,如果是,則表示自己擁有了鎖,不是則繼續輪詢。
當執行緒釋放鎖時,將服務號加1,這樣下一個執行緒看到這個變化,就退出自旋。
簡單的實現
import java.util.concurrent.atomic.AtomicInteger; public class TicketLock { private AtomicInteger serviceNum = new AtomicInteger(); // 服務號 private AtomicInteger ticketNum = new AtomicInteger(); // 排隊號 public int lock() { // 首先原子性地獲得一個排隊號 int myTicketNum = ticketNum.getAndIncrement(); // 只要當前服務號不是自己的就不斷輪詢 while (serviceNum.get() != myTicketNum) { } return myTicketNum; } public void unlock(int myTicket) { // 只有當前執行緒擁有者才能釋放鎖 int next = myTicket + 1; serviceNum.compareAndSet(myTicket, next); } }
缺點
Ticket Lock 雖然解決了公平性的問題,但是多處理器系統上,每個程序/執行緒佔用的處理器都在讀寫同一個變數serviceNum ,每次讀寫操作都必須在多個處理器快取之間進行快取同步,這會導致繁重的系統匯流排和記憶體的流量,大大降低系統整體的效能。
下面介紹的CLH鎖和MCS鎖都是為了解決這個問題的。
MCS 來自於其發明人名字的首字母: John Mellor-Crummey和Michael Scott。
CLH的發明人是:Craig,Landin and Hagersten。
CLH鎖
CLH鎖也是一種基於連結串列的可擴充套件、高效能、公平的自旋鎖,申請執行緒只在本地變數上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private boolean isLocked = true; // 預設是在等待鎖
}
@SuppressWarnings("unused" )
private volatile CLHNode tail ;
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class, CLHNode .class , "tail" );
public void lock(CLHNode currentThreadCLHNode) {
CLHNode preNode = UPDATER.getAndSet( this, currentThreadCLHNode); // 轉載人註釋: 把this裡的"tail" 值設定成currentThreadCLHNode
if(preNode != null) {//已有執行緒佔用了鎖,進入自旋
while(preNode.isLocked ) {
}
}
}
public void unlock(CLHNode currentThreadCLHNode) {
// 如果佇列裡只有當前執行緒,則釋放對當前執行緒的引用(for GC)。
if (!UPDATER .compareAndSet(this, currentThreadCLHNode, null)) {
// 還有後續執行緒
currentThreadCLHNode. isLocked = false ;// 改變狀態,讓後續執行緒結束自旋
}
}
}
MCS鎖
MCS Spinlock 是一種基於連結串列的可擴充套件、高效能、公平的自旋鎖,申請執行緒只在本地變數上自旋,直接前驅負責通知其結束自旋,從而極大地減少了不必要的處理器快取同步的次數,降低了匯流排和記憶體的開銷。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
MCSNode next;
boolean isLocked = true; // 預設是在等待鎖
}
volatile MCSNode queue ;// 指向最後一個申請鎖的MCSNode
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(MCSLock.class, MCSNode. class, "queue" );
public void lock(MCSNode currentThreadMcsNode) {
MCSNode predecessor = UPDATER.getAndSet(this, currentThreadMcsNode);// step 1
if (predecessor != null) {
predecessor.next = currentThreadMcsNode;// step 2
while (currentThreadMcsNode.isLocked ) {// step 3
}
}
}
public void unlock(MCSNode currentThreadMcsNode) {
if ( UPDATER.get( this ) == currentThreadMcsNode) {// 鎖擁有者進行釋放鎖才有意義
if (currentThread.next == null) {// 檢查是否有人排在自己後面
if (UPDATER.compareAndSet(this, currentThreadMcsNode, null)) {// step 4
// compareAndSet返回true表示確實沒有人排在自己後面
return;
} else {
// 突然有人排在自己後面了,可能還不知道是誰,下面是等待後續者
// 這裡之所以要忙等是因為:step 1執行完後,step 2可能還沒執行完
while (currentThreadMcsNode.next == null) { // step 5
}
}
}
currentThreadMcsNode.next.isLocked = false;
currentThreadMcsNode.next = null;// for GC
}
}
}
CLH鎖 與 MCS鎖 的比較
下圖是CLH鎖和MCS鎖佇列圖示:
差異:
- 從程式碼實現來看,CLH比MCS要簡單得多。
- 從自旋的條件來看,CLH是在前驅節點的屬性上自旋,而MCS是在本地屬性變數上自旋
- 從連結串列佇列來看,CLH的佇列是隱式的,CLHNode並不實際持有下一個節點;MCS的佇列是物理存在的。
- CLH鎖釋放時只需要改變自己的屬性,MCS鎖釋放則需要改變後繼節點的屬性。
注意:這裡實現的鎖都是獨佔的,且不能重入的。