Java 并发编程学习笔记 理解CLH队列锁算法

CLH算法实现

CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁。结点之间是通过隐形的链表相连,之所以叫隐形的链表是因为这些结点之间没有明显的next指针,而是通过myPred所指向的结点的变化情况来影响myNode的行为。CLHLock上还有一个尾指针,始终指向队列的最后一个结点。CLHLock的类图如下所示:  
《Java 并发编程学习笔记 理解CLH队列锁算法》    
当一个线程需要获取锁时,会创建一个新的QNode,将其中的locked设置为true表示需要获取锁,然后线程对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前趋的引用myPred,然后该线程就在前趋结点的locked字段上旋转,直到前趋结点释放锁。当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前趋结点。如下图所示,线程A需要获取锁,其myNode域为true,些时tail指向线程A的结点,然后线程B也加入到线程A后面,tail指向线程B的结点。然后线程A和B都在它的myPred域上旋转,一量它的myPred结点的locked字段变为false,它就可以获取锁扫行。明显线程A的myPred locked域为false,此时线程A获取到了锁。  
《Java 并发编程学习笔记 理解CLH队列锁算法》        
整个CLH的代码如下,其中用到了ThreadLocal类,将QNode绑定到每一个线程上,同时用到了AtomicReference,对尾指针的修改正是调用它的getAndSet()操作来实现的,它能够保证以原子方式更新对象引用。  

 1 public class CLHLock {  2     
 3     AtomicReference<QNode> tail = new AtomicReference<QNode>(new QNode());  4     ThreadLocal<QNode> myPred;  5     ThreadLocal<QNode> myNode;  6   
 7     public static class QNode {  8         //注意这个地方 如果不加volatile则会导致线程永远死循环  9         //关于volatile的用法在我的另外一篇文章 http://www.cnblogs.com/daxin/p/3364014.html
10         public volatile boolean locked = false; 11  } 12     
13     public CLHLock() { 14         myNode = new ThreadLocal<QNode>() { 15             protected QNode initialValue() { 16                 return new QNode(); 17  } 18  }; 19         myPred = new ThreadLocal<QNode>() { 20             protected QNode initialValue() { 21                 return null; 22  } 23  }; 24  } 25   
26     public void lock() { 27         QNode qnode = myNode.get(); 28         qnode.locked = true; 29         QNode pred = tail.getAndSet(qnode); 30  myPred.set(pred); 31         while (pred.locked) { 32             //非阻塞算法
33  } 34  } 35   
36     public void unlock() { 37         QNode qnode = myNode.get(); 38         qnode.locked = false; 39  myNode.set(myPred.get()); 40  } 41 } 

 

 
从代码中可以看出lock方法中有一个while循环,这 是在等待前趋结点的locked域变为false,这是一个自旋等待的过程。unlock方法很简单,只需要将自己的locked域设置为false即可。

CLH优缺点

CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail),CLH的一种变体被应用在了JAVA并发框架中。唯一的缺点是在NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣,但是在SMP系统结构下该法还是非常有效的。一种解决NUMA系统结构的思路是MCS队列锁。

    原文作者:大新博客
    原文地址: https://www.cnblogs.com/daxin/p/3365324.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞