目錄
自旋鎖
自旋鎖是指當一個線程嘗試獲取某個鎖時,如果該鎖已被其他線程佔用,就一直循環檢測鎖是否被釋放,而不是進入線程掛起或睡眠狀態。
自旋鎖適用於鎖保護的臨界區很小的情況,臨界區很小的話,鎖佔用的時間就很短。
簡單實現
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 ,每次讀寫操作都必須在多個處理器緩存之間進行緩存同步,這會導致繁重的系統總線和內存的流量,大大降低系統整體的性能。
MCS鎖
MCS Spinlock 是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,直接前驅負責通知其結束自旋,從而極大地減少了不必要的處理器緩存同步的次數,降低了總線和內存的開銷。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isBlock = true; // 默認是在等待鎖
}
volatile MCSNode queue;// 指向最後一個申請鎖的MCSNode
private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MCSNode.class, "queue");
public void lock(MCSNode currentThread) {
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
if (predecessor != null) {
predecessor.next = currentThread;// step 2
while (currentThread.isBlock) {// step 3
}
}else { // 只有一個線程在使用鎖,沒有前驅來通知它,所以得自己標記自己爲非阻塞
currentThread. isBlock = false;
}
}
public void unlock(MCSNode currentThread) {
if (currentThread.isBlock) {// 鎖擁有者進行釋放鎖纔有意義
return;
}
if (currentThread.next == null) {// 檢查是否有人排在自己後面
if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
// compareAndSet返回true表示確實沒有人排在自己後面
return;
} else {
// 突然有人排在自己後面了,可能還不知道是誰,下面是等待後續者
// 這裏之所以要忙等是因爲:step 1執行完後,step 2可能還沒執行完
while (currentThread.next == null) { // step 5
}
}
}
currentThread.next.isBlock = false;
currentThread.next = null;// for GC
}
}
CLH鎖
CLH鎖也是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private volatile 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 currentThread) {
CLHNode preNode = UPDATER.getAndSet( this, currentThread);
if(preNode != null) {//已有線程佔用了鎖,進入自旋
while(preNode.isLocked ) {
}
}
}
public void unlock(CLHNode currentThread) {
// 如果隊列裏只有當前線程,則釋放對當前線程的引用(for GC)。
if (!UPDATER .compareAndSet(this, currentThread, null)) {
// 還有後續線程
currentThread. isLocked = false ;// 改變狀態,讓後續線程結束自旋
}
}
}
CLH鎖 與 MCS鎖 的比較
- 從代碼實現來看,CLH比MCS要簡單得多。
- 從自旋的條件來看,CLH是在前驅節點的屬性上自旋,而MCS是在本地屬性變量上自旋。
- 從鏈表隊列來看,CLH的隊列是隱式的,CLHNode並不實際持有下一個節點;MCS的隊列是物理存在的。
- CLH鎖釋放時只需要改變自己的屬性,MCS鎖釋放則需要改變後繼節點的屬性。
驚羣效應
驚羣效應也有人叫做雷鳴羣體效應,不過叫什麼,簡言之,驚羣現象就是多進程(多線程)在同時阻塞等待同一個事件的時候(休眠狀態),如果等待的這個事件發生,那麼他就會喚醒等待的所有進程(或者線程),但是最終卻只可能有一個進程(線程)獲得這個時間的“控制權”,對該事件進行處理,而其他進程(線程)獲取“控制權”失敗,只能重新進入休眠狀態,這種現象和性能浪費就叫做驚羣。
爲了更好的理解何爲驚羣,舉一個很簡單的例子,當你往一羣鴿子中間扔一粒穀子,所有的各自都被驚動前來搶奪這粒食物,但是最終註定只可能有一個鴿子滿意的搶到食物,沒有搶到的鴿子只好回去繼續睡覺,等待下一粒穀子的到來。這裏鴿子表示進程(線程),那粒穀子就是等待處理的事件。
CLH的FIFO等待隊列給解決在鎖競爭方面的驚羣效應問題提供了一個思路:保持一個FIFO隊列,隊列每個節點只關心其前一個節點的狀態,線程喚醒也只喚醒隊頭等待線程。
轉載自:https://coderbee.net/index.php/concurrent/20131115/577