自己动手实现四种自旋锁( 二 )


这个转化过程有两个要点:一是应该构建怎样的队列以及如何构建队列?为了保证公平性,我们构建的将是一个FIFO队列 。构建的时候主要通过移动尾部节点tail来实现队列的排队,每个想获取锁的线程创建一个新节点并通过CAS原子操作将新节点赋给tail,然后让当前线程轮询前一节点的某个状态位 。如图可以清晰看到队列结构及自旋操作,这样就成功构建了线程排队队列 。二是如何释放队列?执行完线程后只需将当前线程对应的节点状态位置为解锁状态即可,由于下一节点一直在轮询,所以可获取到锁 。

自己动手实现四种自旋锁

文章插图
 
所以,CLH锁的核心思想是将众多线程长时间对某资源的竞争,通过有序化这些线程将其转化为只需对本地变量检测 。而唯一存在竞争的地方就是在入队列之前对尾节点tail的竞争,但此时竞争的线程数量已经少了很多了 。比起所有线程直接对某资源竞争的轮询次数也减少了很多,这也大大节省了CPU缓存同步的消耗,从而大大提升系统性能 。
下面我们提供一个简单的CLH锁实现代码,以便更好理解CLH锁的原理 。其中lock与unlock两方法提供加锁和解锁操作,每次加锁解锁必须将一个CLHNode对象作为参数传入 。lock方法的for循环是通过CAS操作将新节点插入队列,而while循环则是检测前驱节点的锁状态位 。一旦前驱节点锁状态位允许则结束检测,让线程往下执行 。解锁操作先判断当前节点是否为尾节点,如是则直接将尾节点设置为空,此时说明仅仅只有一条线程在执行,否则将当前节点的锁状态位设置为解锁状态 。
1. public class CLHLock {
2. private static Unsafe unsafe = null;
3. private static final long valueOffset;
4. private volatile CLHNode tail;
5.
6. public class CLHNode {
7. private volatile boolean isLocked = true;
8. }
9.
10. static {
11. try {
12. unsafe = getUnsafeInstance();
13. valueOffset = unsafe.objectFieldOffset(CLHLock.class.getDeclaredField("tail"));
14. } catch (Exception ex) {
15. throw new Error(ex);
16. }
17. }
18.
19. public void lock(CLHNode currentThreadNode) {
20. CLHNode preNode = null;
21. for (;;) {
22. preNode = tail;
23. if (unsafe.compareAndSwapObject(this, valueOffset, preNode, currentThreadNode))
24. break;
25. }
26. if (preNode != null)
27. while (preNode.isLocked) {
28. }
29. }
30.
31. public void unlock(CLHNode currentThreadNode) {
32. if (!unsafe.compareAndSwapObject(this, valueOffset, currentThreadNode, null))
33. currentThreadNode.isLocked = false;
34. }
35.
36. private static Unsafe getUnsafeInstance() throws Exception {
37. Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
38. theUnsafeInstance.setAccessible(true);
39. return (Unsafe) theUnsafeInstance.get(Unsafe.class);
40. }
41. }
MCS锁MCS锁由John Mellor-Crummey和Michael Scott两人发明,它的出现旨在解决CLH锁存在的问题 。它也是基于FIFO队列,与CLH锁相似,不同的地方在于轮询的对象不同 。MCS锁中线程只对本地变量自旋,而前驱节点则负责通知其结束自旋操作 。这样的话就减少了CPU缓存与主存储之间的不必要的同步操作,减少了同步带来的性能损耗 。
如下图,每个线程对应着队列中的一个节点 。节点内有一个spin变量,表示是否需要旋转 。一旦前驱节点使用完锁后便修改后继节点的spin变量,通知其不必继续做自旋操作,已成功获取锁 。
自己动手实现四种自旋锁

文章插图
 
下面我们提供一个简单的MCS锁实现代码,以便更好理解MCS锁的原理 。其中lock与unlock两方法提供加锁和解锁操作,每次加锁解锁必须将一个MCSNode对象作为参数传入 。lock方法的for循环是通过CAS操作将新节点赋给队列尾部节点tail,如果存在前驱节点的话则自己要开始自旋操作,等待前驱节点解锁时通知自己 。一旦前驱节点执行解锁后,则会将本节点的spin变量修改为false,本节点则获取到锁并停止自旋,让线程往下执行 。解锁操作先判断当前节点是否为尾节点,如果是则什么都不用处理 。此时说明仅仅只有一条线程在执行,否则将后继节点的spin变量设置为false 。此时要考虑特殊情况,如果不存在后继节点则将尾部节点tail设为null 。在此期间可能又有线程进来,这时tail的CAS修改会失败,所以就只能自旋等后继节点不为空再往下执行 。


推荐阅读