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