在學習Java AQS框架的時候發現加鎖的邏輯很是奇怪,後來得知加鎖邏輯是CLH鎖的一個變種,因而瞭解一下,對於理解AQS框架有好處。java
CLH鎖是有由Craig, Landin, and Hagersten這三我的發明的鎖,取了三我的名字的首字母,因此叫 CLH Lock。node
CLH鎖主要有一個QNode類,QNode類內部維護了一個boolean類型的變量,每一個線程擁有一個前驅節點(myPred)和當前本身的節點(myNode),還有一個tail節點用於存儲最後一個獲取鎖的線程的狀態。CLH的從邏輯上造成一個鎖等待隊列從而實現加鎖,CLH鎖只支持按順序加鎖和解鎖,不支持重入,不支持中斷。多線程
public class CLHLock { private final AtomicReference<QNode> tail; private final ThreadLocal<QNode> myPred; private final ThreadLocal<QNode> myNode; private static class QNode { volatile boolean locked = false; } public CLHLock() { tail = new AtomicReference<QNode>(new QNode()); myNode = new ThreadLocal<QNode>() { @Override protected QNode initialValue() { return new QNode(); } }; myPred = new ThreadLocal<QNode>() { @Override protected QNode initialValue() { return null; } }; } public void lock() { QNode node = myNode.get(); node.locked = true; QNode pred = tail.getAndSet(node); myPred.set(pred); while (pred.locked) {} } public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; myNode.set(myPred.get()); // myNode.set(new QNode()); } }
代碼很簡單,tail變量的類型是AtomicReference用於保證原子操做,myNode是ThreadLocal類型的線程本地變量,保存當前節點的狀態,myPred是ThreadLocal類型的線程本地變量,保存等待節點的狀態。框架
先經過簡單的測試看一下效果ide
public static void main(String[] args) { Runnable runnable = new Runnable() { private int a; @Override public void run() { for (int i = 0; i < 10000; i++) { a++; } System.out.println(Thread.currentThread().getName() + " a = " + a); } }; new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); }
聲明瞭一個runnable對象,在線程內執行從1累加到10000,最後打印一個結果。在多線程的環境下這個a++不是一個原子操做,因此最後的計算結果必定是不正確的。學習
Thread-0 a = 11758 Thread-1 a = 15091 Thread-2 a = 18309 Thread-3 a = 18831 Thread-4 a = 23398 Thread-5 a = 23686 Thread-6 a = 33686
運行一次以後是這樣的結果,和預期同樣。而後加上鎖看一下測試
public static void main(String[] args) { CLHLock lock = new CLHLock(); Runnable runnable = new Runnable() { private int a; @Override public void run() { lock.lock(); for (int i = 0; i < 10000; i++) { a++; } System.out.println(Thread.currentThread().getName() + " a = " + a); lock.unlock(); } }; new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); }
建立了一個CLHLock對象,調用了 lock.lock() 和 lock.unlock()。把整個run方法裏面的內容都鎖住,也就是等一個線程運行完了這個累加,下一個線程才能夠繼續執行,不然只能等着。spa
Thread-0 a = 10000 Thread-1 a = 20000 Thread-2 a = 30000 Thread-3 a = 40000 Thread-4 a = 50000 Thread-5 a = 60000 Thread-6 a = 70000
如今屢次運行以後都是這個結果,加鎖有效果。線程
咱們仔細分析一下lock和unlock的代碼code
public void lock() { QNode node = myNode.get(); node.locked = true; QNode pred = tail.getAndSet(node); myPred.set(pred); while (pred.locked) {} } public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; myNode.set(myPred.get()); }
鎖的代碼很簡單就這麼幾行
結合這個圖從上往下看,場景是有2個線程(Thread1, Thread2)同時想獲取鎖執行任務,左邊是Thread1的執行狀況,右邊是Thread2的執行狀況。這裏的myNode myPred都是threadlocal類型的,下面說的myNode myPred的狀態都是指myNode myPred內的QNode的狀態。
第一行是初始化以後的狀態,各個QNode都是false。
第二行第三行開始執行lock的操做,先將myNode的狀態改成true,再將myNode的引用賦值給tail(tail.getAndSet(node) 的意思是將tail設置爲node並返回tail原來的值,這裏tail存的是一個QNode對象),再把tail原來的值賦值給myPred,經過一個while循環判斷myPred的狀態是否爲true,爲true表示鎖正在被佔用須要等待,一旦myPred變爲false表示鎖被釋放了,能夠執行。那麼結合2個線程的狀況來看,thread1調用lock方法成功獲取到鎖,thread2同時也調用lock方法想要獲取鎖,執行到 tail.getAndSet(node)的時候將tail設置爲thread2.myNode,而後獲取tail的舊值設置到thread2.myPred,那這個時候tail的舊值是剛纔thread1的myNode,也就是說thread2在執行 while(pred.locked){} 等待的時候其實等待的是thread1.myNode狀態變爲false。tail存儲的只是最後一個獲取鎖的線程的QNode,myNode一直在myPred上等待,經過一個while循環來實現獨佔鎖。
第四行開始執行unlock操做,thread1任務執行完了將myNode的狀態設置爲false,此時thread2.myPred由於持有的是thread1.myNode的引用,因此也變爲false退出循環,thread2得以執行下面的任務。
第五行,將myNode的值設置爲myPred的引用。
看上去第五行彷佛沒有什麼必要,網上關於這個的說話比較多,說一下個人理解。若是沒有這行代碼,在上面這個圖中thread2線程在等待thread1.myNode的狀態,假設thread1任務執行的速度很是快,在thread2的while的一次判斷以後下一次判斷開始以前,thread1執行完任務調用unlock解鎖,而後立刻又申請鎖調用lock,又將thread1.myNode的狀態設置爲true了,同時thread1將tail值設置爲thread1.myPred(這個時候tail節點儲存的是thread2.myNode的引用),如此一來2個線程就變成了一個相互等待的狀況,即死鎖。那麼在unlock的時候執行了myNode.set(myPred.get());的話,如今的myNode和thread2的myPred已經不是一個對象了,因此thread2.myPred會由於第四行的qnode.locked=false;退出循環等待。我的拙見,這裏myNode.set(myPred.get());替換成myNode.set(new QNode());效果是同樣的。
這裏的死鎖發生的狀況有必定的特殊性,myNode myPred是ThreadLocall類型的,而在線程池的場景下爲了線程複用Thread一旦被建立就不會銷燬,因此ThreadLocal類型的變量使用完必定要手動清理(下次執行以前若是不手動清理,ThreadLocal類型的變量仍是上一次執行的結果),上面的第五行代碼其實也是ThreadLocal使用完清理變量的意思,若是不使用線程池的話即便沒有第五行代碼也不會死鎖。
public class CLHLock { ... public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; //myNode.set(myPred.get()); } ... } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); CLHLock lock = new CLHLock(); Runnable runnable = new Runnable() { private int a; @Override public void run() { lock.lock(); for (int i = 0; i < 100; i++) { a++; } System.out.println(Thread.currentThread().getName() + " a = " + a); lock.unlock(); } }; executorService.execute(runnable); executorService.execute(runnable); executorService.execute(runnable); executorService.execute(runnable); executorService.execute(runnable); executorService.execute(runnable); executorService.shutdown(); }