本文为原创,转载请注明出处
1、为什么要引入CLH锁
在前一篇文章中,介绍了TAS、TTAS两种自旋锁。这两种锁的缺点是:任何一个处理器每一次对锁成功的访问(getAndSet(true)和set(false)任意一个方法的调用),都会将其他处理器的cache中的缓存失效掉。这样会导致以下后果:
- 其他处理器无法再采用局部自旋的方式对相同数据进行访问,后续的其他处理器对锁的访问都会独占bus发送消息。而锁的释放也是需要占用bus,因此有可能其他处理器对bus的占用而导致锁的释放被延迟;
- 当锁释放后的一刹那,其他正在局部自旋的处理器同时竞争去获取锁(调用getAndSet(true)),原本只有一个处理器能在此次竞争中获胜,但其他获取失败的处理器还是在bus上制造了大量无用的交通阻塞,因此也会导致锁的释放被延迟;
- 由于数据访问存在局部性,cache从memory中加载数据时,一次会加载一个数据块,即cache存储的是cache line。Cache中锁状态数据的失效,会导致其他和锁状态处于同一个cache line中的数据也被失效掉,从而影响其他原本与锁不存在竞争的处理器的执行速度。
- 另外,最后一个缺点就是无法实现锁的公平性,最先自旋等待获取锁的处理器,在竞争中,不一定最先获取到锁。
虽然基于时间延迟的BackoffTTAS锁能一定程度的降低cache被无故失效的次数,但如何设置最优的延迟时间却是一个难题,因为不同的平台会有不同的最优参数,同时,引入延迟后,有可能导致处理器不能及时的得到锁,造成无谓的等待。 那么我们希望最理想的情况是:
- 由于锁(此处指排它锁)同一时刻只能由一个处理器持有,所以在锁释放后的一刹那,最多只需要通知一个正在等待持有锁的处理器去获取锁,其他处理器就不需要同时去凑热闹了,轮到它的时候,会自动只通知他。
- 当某个处理器获取到锁或者释放锁时,不要失效掉其他还未轮到顺序的处理器的cache,这样这些处理器就能一直在自己的cache中局部自旋,不会占用bus。
CLH锁就是基于这种理念设计出来的
2、CLH锁介绍
CLH算法中,每个申请锁的线程通过一个QNode对象表示,QNode中有一个volatile boolean类型的成员变量locked,locked为true,则表示对应的线程已经获取到了锁或者正在等待获取锁;locked为false,则表示对应的线程已经释放了锁。
public class QNode {
public volatile boolean locked = false;
}
而锁则由多个QNode对象组成的虚拟链表表示,之所以称为虚拟链表,是因为QNode之间并没有类似于next指针之类的引用,QNode之间通过锁的一个本地线程(ThreadLocal)变量myPred相连,myPred指向当前节点的前驱节点,即当前线程的前一个线程。而链表的尾节点则通过锁AtomicReference<QNode>类型的tail成员变量指向,即tail指向加入到申请锁的队列中的最近一个线程。
public class CLHLock implements Lock {
private AtomicReference<QNode> tail;
private ThreadLocal<QNode> myNode;
private ThreadLocal<QNode> myPred;
public CLHLock() {
tail = new AtomicReference<QNode>(new QNode());
myNode = new ThreadLocal<QNode>() {
protected QNode initialValue() {
return new QNode();
}
};
myPred = new ThreadLocal<QNode>() {
protected QNode initialValue() {
return null;
}
};
}
public void lock() {
QNode qnode = myNode.get();
qnode.locked = true;
QNode pred = tail.getAndSet(qnode);
myPred.set(pred);
while (pred.locked) {}
}
public void unlock() {
QNode qnode = myNode.get();
qnode.locked = false;
myNode.set(myPred.get());
}
}
当一个线程申请锁时:
- 首先会实例化一个QNode节点,并将其设置为自己的本地线程变量myNode;
- 然后将myNode的locked域设置为true,表明该线程正在等待获取锁;
- 接着通过AtomicReference的getAndSet()方法,将myNode设置为队列的最后一个节点,并返回其前驱节点;
- 最后该线程一直在其前驱节点的locked域自旋,直到locked域变为false,即前驱节点释放了锁。注意,对于SMP架构,由于是在cache中自旋,所以是高效的;
当一个线程释放锁时:
- 将myNode的locked域设置为false,使得后继线程停止自旋以便获取到锁;
- 然后重用前驱节点,将前驱节点设置为myNode,以便下一次该线程获取锁时使用。之所以能重用,是因为此时其前驱线程不会再使用该前驱节点了。而该线程原来的myNode,可能被其后继线程或tail引用。对于java语言来说,重用实际上是没有必要的,因为如果不重用,则前驱节点是可以被jvm回收的,从而降低内存的使用。
下图阐述了所得获取和释放过程(引自The art of Multiprocessor Programming一书)
3、结论
CLH算法将多个线程的自旋分散在不同的节点进行,当一个线程释放锁时,只会将其后继结点的cache失效掉,对于其他线程没有任何影响,其他线程还是在各自的cache中进行局部自旋。这样,就大大减少了bus上的交通阻塞,不会导致锁的释放被延迟。
另外,CLH算法由于总是将最近一个申请锁的线程放在链表的最后,从而带来了一个附加的功能,那就是对锁竞争的公平性,保证了先申请锁的线程先得到锁。
而且,CHL算法也是空间有效的,对于N个线程,L个锁,如果每个线程每次最多只能获取一个锁,则需要的存储空间是O(N+L),其中N个线程对应N个myNode,L个锁对应L个tail。
当然,CLH算法也有一个缺点,在cache-less的NUMA架构体系下,因为每个线程自旋的是其前驱线程的QNode中的locked域,如果内存位置比较远,则性能是要打折扣的。
- 大小: 128.4 KB
分享到:
相关推荐
C语言高级编程技术(CLH)C语言高级编程技术(CLH)C语言高级编程技术(CLH)C语言高级编程技术(CLH)C语言高级编程技术(CLH)C语言高级编程技术(CLH)C语言高级编程技术(CLH)C语言高级编程技术(CLH)C语言高级编程技术(CLH)...
C语言也能做大事1笔记(CLH)C语言也能做大事1笔记(CLH)C语言也能做大事1笔记(CLH)C语言也能做大事1笔记(CLH)C语言也能做大事1笔记(CLH)C语言也能做大事1笔记(CLH)C语言也能做大事1笔记(CLH)C语言也能做大事1笔记(CLH)...
3. **CLH锁** ReentrantLock采用的是CLH(Craig, Landin, and Hagersten)队列,这是一种高效的自旋锁。每个等待的线程都会封装成一个Node节点,Node包含了线程信息、等待状态(如0表示等待,-1表示阻塞)以及前后...
- **锁机制**:自旋锁、阻塞锁、乐观锁、悲观锁,以及MCS锁和CLH锁队列的应用。 - **Java线程池**:线程池的工作原理,线程数的设置考虑因素,如任务性质(计算密集型或IO密集型)。 4. **数据结构**: - **集合...
4. **CLH锁队列**:AQS的等待队列采用CLH(Craig, Landin, and Hagersten)锁队列结构,这是一种高效的自旋锁实现,每个节点代表一个线程,前驱节点即为阻塞它的线程。 5. **节点状态**:节点状态包括PARKED(已...
13. **ReentrantLock的实现**:基于AQS(AbstractQueuedSynchronizer),使用CLH锁(自旋锁)维护一个双向队列,线程通过不断轮询前一个节点状态等待锁的释放。 14. **RDB快照**:Redis使用bgSAVE命令创建子进程...
- **排队机制**:AQS内部使用CLH锁队列(Craig-Landin-Hagersten lock queue)作为线程等待队列的基础结构。每个等待同步状态改变的线程会被封装成一个节点(`Node`),并加入到队列中。当线程获得同步状态后,会从...
比如,使用高效的并发控制算法(如CLH锁、读写锁)和数据结构(如ConcurrentHashMap)。 8. **前端优化**:使用CDN加速静态资源的加载,使用Ajax实现页面异步加载,减少用户等待时间。还可以通过预加载、预渲染等...
书中会介绍各种并发控制机制,如互斥锁、条件变量、读写锁,以及非阻塞同步算法如Peterson算法和CLH锁。 最后,该书也会讨论分布式操作系统和实时操作系统的特点,以及现代操作系统中的安全性和隐私保护问题。操作...
CLH锁利用了线程局部变量和原子更新操作来实现线程间的同步,当一个线程想要获取锁时,它会创建一个节点并加入链表,然后调用`park()`进入等待状态,直到前驱节点释放锁并唤醒它。 阻塞锁的优势在于,被阻塞的线程...
也对线程池,AQS的基础CLH 锁,提供了简易的实现,帮助理解。 jvm 将深入理解 jvm 的内容与oracle 官网的内容进行了总结,将其分为了几部分。便于查看。 Spring 对spring core、mvc 等模块的源码进行分析。 例如 ...
CLH Lock是一种基于链表的自旋锁机制,它使用一个链表来记录等待获取自旋锁的处理器。当一个处理器尝试获取自旋锁时,它将被添加到链表中,并等待前一个处理器释放自旋锁。 在Linux内核中,自旋锁的实现非常重要,...
`code`文件可能会包含CLHLock的源代码,你可以研究其如何通过节点间的交互来实现自旋等待和锁的释放。 4. **MCSLock(Mellor-Crummey and Scott Lock)**:与CLHLock类似,也是一种链表型自旋锁。MCSLock使用单向...
当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。 在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)。...
在线程获取锁时会调用AQS的acquire()方法,该方法第一次尝试获取锁如果失败,会将该线程加入到CLH队列中:public final void acqui
3. CLHLock(Craig, Landin, and Hagersten Lock)和MCSLock(Mellor-Crummey and Scott Lock):这两种自旋锁是基于链表结构的公平锁,它们将等待线程组织成一个队列,并使用`AtomicReferenceFieldUpdater`等原子...
java8 看不到源码DLock - 分布式锁 ...并且重试线程会周期性的唤醒CLH队列的头线程进行重试,这样每个进程只有一个线程可以参与锁竞争,避免了不必要的锁竞争。 同时,还为吞吐量提供了不公平锁。 快速
在公平锁模式下,线程获取锁的顺序严格按照CLH队列的顺序进行。ReentrantLock的内部结构包含一个Sync对象,Sync是AQS的子类,进一步分为FairSync(公平锁)和NonFairSync(非公平锁)两个子类。 6. **获取公平锁的...
(2)竞争锁失败,线程封装成节点加入CLH双向链表阻塞队列;(3)线程进行自旋,尝试竞争锁,竞争锁失败,阻塞排队的线程节点,需要等待持有锁的线程释放锁unpark线层或者其他方式唤醒线程,线程再尝试获取锁。 3. ...