CLH鎖 簡介

概述

在學習Java AQS框架的時候發現加鎖的邏輯很是奇怪,後來得知加鎖邏輯是CLH鎖的一個變種,因而瞭解一下,對於理解AQS框架有好處。java

簡介

CLH鎖是有由Craig, Landin, and Hagersten這三我的發明的鎖,取了三我的名字的首字母,因此叫 CLH Lock。node

CLH鎖主要有一個QNode類,QNode類內部維護了一個boolean類型的變量,每一個線程擁有一個前驅節點(myPred)和當前本身的節點(myNode),還有一個tail節點用於存儲最後一個獲取鎖的線程的狀態。CLH的從邏輯上造成一個鎖等待隊列從而實現加鎖,CLH鎖只支持按順序加鎖和解鎖,不支持重入,不支持中斷。多線程

Java實現

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();
}

相關文章
相關標籤/搜索