--喜歡記得關注我喲【shoshana】--
前記
JUC中的Lock中最核心的類AQS,其中AQS使用到了CLH隊列的變種,故來研究一下CLH隊列的原理及JAVA實現
一. CLH背景知識
CLH 鎖的名字也與他們的發明人的名字相關:Craig,Landin and Hagersten。
CLH Lock摘要
CLH lock is Craig, Landin, and Hagersten (CLH) locks, CLH lock is a spin lock, can ensure no hunger, provide fairness first come first service.
The CLH lock is a scalable, high performance, fairness and spin lock based on the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.
CLH鎖是自旋鎖的一種,對它的研究是因為AQS源代碼中使用了CLH鎖的一個變種,為了更好的理解AQS中使用鎖的思想,所以決定先好好理解CLH鎖
二. CLH原理
CLH也是一種基於單向鏈表(隱式創建)的高性能、公平的自旋鎖,申請加鎖的線程只需要在其前驅節點的本地變量上自旋,從而極大地減少了不必要的處理器緩存同步的次數,降低了總線和內存的開銷。
三. Java代碼實現
類圖
public interface Lock {
void lock();
void unlock();
}
public class QNode {
volatile boolean locked;
}
import java.util.concurrent.atomic.AtomicReference;
public class CLHLock implements Lock {
// 尾巴,是所有線程共有的一個。所有線程進來后,把自己設置為tail
private final AtomicReference<QNode> tail;
// 前驅節點,每個線程獨有一個。
private final ThreadLocal<QNode> myPred;
// 當前節點,表示自己,每個線程獨有一個。
private final ThreadLocal<QNode> myNode;
public CLHLock() {
this.tail = new AtomicReference<QNode>(new QNode());
this.myNode = new ThreadLocal<QNode>() {
protected QNode initialValue() {
return new QNode();
}
};
this.myPred = new ThreadLocal<QNode>();
}
@Override
public void lock() {
// 獲取當前線程的代表節點
QNode node = myNode.get();
// 將自己的狀態設置為true表示獲取鎖。
node.locked = true;
// 將自己放在隊列的尾巴,並且返回以前的值。第一次進將獲取構造函數中的那個new QNode
QNode pred = tail.getAndSet(node);
// 把舊的節點放入前驅節點。
myPred.set(pred);
// 判斷前驅節點的狀態,然后走掉。
while (pred.locked) {
}
}
@Override
public void unlock() {
// unlock. 獲取自己的node。把自己的locked設置為false。
QNode node = myNode.get();
node.locked = false;
myNode.set(myPred.get());
}
}
簡單的看一下CLH的算法定義
the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.
基於list,線程僅在一個局部變量上自旋,它不斷輪詢前一個節點狀態,如果發現前一個節點釋放鎖結束.
所以在java中使用了ThreadLocal作為具體實現,AtomicReference為了消除多個線程並發對tail引用Node的影響,核心方法lock()中分為3個步驟去實現
-
初始狀態 tail指向一個node(head)節點
private final AtomicReference<Node> tail = new AtomicReference<Node>(new Node());
-
thread加入等待隊列: tail指向新的Node,同時Prev指向tail之前指向的節點,在java代碼中使用了getAndSet即CAS操作使用
Node pred = this.tail.getAndSet(node); this.prev.set(pred);
-
尋找當前線程對應的node的前驅node然后開始自旋前驅node的status判斷是否可以獲取lock
while (pred.locked);
同理unlock()方法,獲取當前線程的node,設置lock status,將當前node指向前驅node(這樣操作tail指向的就是前驅node等同於出隊操作).至此CLH Lock的過程就結束了
測試CLHLock
public class CLHLockDemo2 {
public static void main(String[] args) {
final Kfc kfc = new Kfc();
for (int i = 0; i < 10; i++) {
new Thread("eat" + i) {
public void run() {
kfc.eat();
}
}.start();
}
}
}
class Kfc {
private final Lock lock = new CLHLock();
private int i = 0;
public void eat() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": " + --i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void cook() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": " + ++i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
運行結果
eat1: -1
eat0: -2
eat3: -3
eat4: -4
eat7: -5
eat2: -6
eat5: -7
eat6: -8
eat8: -9
eat9: -10
四. CLH優缺點
CLH隊列鎖的優點是空間復雜度低(如果有n個線程,L個鎖,每個線程每次只獲取一個鎖,那么需要的存儲空間是O(L+n),n個線程有n個myNode,L個鎖有L個tail),CLH的一種變體被應用在了JAVA並發框架中。唯一的缺點是在NUMA系統結構下性能很差,在這種系統結構下,每個線程有自己的內存,如果前趨結點的內存位置比較遠,自旋判斷前趨結點的locked域,性能將大打折扣,但是在SMP系統結構下該法還是非常有效的。一種解決NUMA系統結構的思路是MCS隊列鎖。
五. 了解與CLH對應的MCS自旋鎖
MCS 自旋鎖
MCS 的名稱來自其發明人的名字:John Mellor-Crummey和Michael Scott。
MCS 的實現是基於鏈表的,每個申請鎖的線程都是鏈表上的一個節點,這些線程會一直輪詢自己的本地變量,來知道它自己是否獲得了鎖。已經獲得了鎖的線程在釋放鎖的時候,負責通知其它線程,這樣 CPU 之間緩存的同步操作就減少了很多,僅在線程通知另外一個線程的時候發生,降低了系統總線和內存的開銷。實現如下所示:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; public class MCSLock { public static class MCSNode { volatile MCSNode next; volatile boolean isWaiting = true; // 默認是在等待鎖 } volatile MCSNode queue;// 指向最后一個申請鎖的MCSNode private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> 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.isWaiting) {// step 3 } } else { // 只有一個線程在使用鎖,沒有前驅來通知它,所以得自己標記自己已獲得鎖 currentThread.isWaiting = false; } } public void unlock(MCSNode currentThread) { if (currentThread.isWaiting) {// 鎖擁有者進行釋放鎖才有意義 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.isWaiting = false; currentThread.next = null;// for GC } }
MCS 的能夠保證較高的效率,降低不必要的性能消耗,並且它是公平的自旋鎖。
CLH 鎖與 MCS 鎖最大的不同是,MCS 輪詢的是當前隊列節點的變量,而 CLH 輪詢的是當前節點的前驅節點的變量,來判斷前一個線程是否釋放了鎖。
小結
CLH Lock是一種比較簡單的自旋鎖算法之一,因為鎖的CAS操作涉及到了硬件的鎖定(鎖總線或者是鎖內存)所以性能和CPU架構也密不可分,該興趣的同學可以繼續深入研究包括MCS鎖等。CLH Lock是獨占式鎖的一種,並且是不可重入的鎖,這篇文章是對AQS鎖源代碼分析的預熱篇
參考內容:
https://segmentfault.com/a/1190000007094429
https://blog.csdn.net/faicm/article/details/80501465
https://blog.csdn.net/aesop_wubo/article/details/7533186
https://www.jianshu.com/p/0f6d3530d46b
https://blog.csdn.net/jjavaboy/article/details/78603477