本篇文章主要记录自旋锁、CLH锁、MCS锁的学习所得。关于自旋锁和CLH锁、MCS锁,网上已经有很多内容,而且很类似;学习就是学习前人经验,理解、总结,化为己用,因此,虽然网上有很多相关的内容,我也是参考了这些内容,我依然选择记录下了自己的理解,方便自己查阅。
一. 自旋锁
自旋锁(SpinLock):多个线程,当一个线程尝试获取锁的时候,如果锁被占用,就在当前线程循环检查锁是否被释放,这个时候线程并没有进入休眠或者挂起。
代码实现
下面是自旋锁的简单实现:
1 | package io.github.brightloong.concurrent.lock; |
- 使用AtomicReference保存当前获取到锁的线程,保证了compareAndSet操作的原子性,关于CAS请参考——Java中CAS学习记录
缺点
- 非公平锁,并不能保证线程获取锁的顺序。
- 保证各个CPU的缓存(L1、L2、L3、跨CPU Socket、主存)的数据一致性,通讯开销很大,在多处理器系统上更严重。
- 没法保证公平性,不保证等待进程/线程按照FIFO顺序获得锁。
二. CLH锁
CLH锁:Craig, Landin, and Hagersten (好吧,这是三个人的名字),它是基于链表实现的自旋锁,它不断的轮询前驱的状态,如果前驱释放锁,它就结束自旋转。
实现
CLH锁实现如下:
- 线程持有自己的node变量,node中有一个locked属性,true代表需要锁,false代表不需要锁。
- 线程持有前驱的node引用,轮询前驱node的locked属性,true的时候自旋,false的时候代表前驱释放了锁,结束自旋。
- tail始终指向最后加入的线程。
运行原理
其运行用下图做一个说明:
- 初始化的时候tail指向一个类似head的节点,此时node的locked属性为false,preNode为空。
- 当线程A进来的时候,线程A持有的node节点,node的locked属性为true,preNode指向之前的head节点。
- 当线程B进来的时候,线程B持有的node节点,node的locked属性为true,preNode指向线程A的node节点,线程B的node节点locked属性为true,线程A轮询线程B的node节点的locked状态,为true自旋。
- 线程A执行完后释放锁(修改locked属性为false),线程B轮询到线程A的node节点locked属性为false,结束自旋。
代码实现
CLH自旋锁代码实现:
1 | package io.github.brightloong.concurrent.lock; |
- Node中只有一个locked属性,默认false
- ThreadLocal
node,当前线程的节点,ThreadLocal实现了变量的线程隔离,关于ThreadLocal可以参考——ThreadLocal源码分析 - ThreadLocal
preNode,前驱节点
优点
- 是公平锁,FIFO
- CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n)
缺点
介绍缺点前先说一下NUMA和SMP两种处理器结构
SMP(Symmetric Multi-Processor),即对称多处理器结构,指服务器中多个CPU对称工作,每个CPU访问内存地址所需时间相同。其主要特征是共享,包含对CPU,内存,I/O等进行共享。SMP的优点是能够保证内存一致性,缺点是这些共享的资源很可能成为性能瓶颈,随着CPU数量的增加,每个CPU都要访问相同的内存资源,可能导致内存访问冲突,可能会导致CPU资源的浪费。常用的PC机就属于这种。
NUMA(Non-Uniform Memory Access)非一致存储访问,将CPU分为CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、I/O槽口等,模块之间可以通过互联模块相互访问,访问本地内存的速度将远远高于访问远地内存(系统内其它节点的内存)的速度,这也是非一致存储访问NUMA的由来。NUMA优点是可以较好地解决原来SMP系统的扩展问题,缺点是由于访问远地内存的延时远远超过本地内存,因此当CPU数量增加时,系统性能无法线性增加。
现在说CLH锁的缺点是在NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣。
另一种实现
CLH自旋锁在网上还有一种使用AtomicReferenceFieldUpdater实现的版本,这里也把代码贴出来,个人认为这种版本相比较上面的那种实现更难理解,在代码中添加了一些注释帮助理解。
1 | package io.github.brightloong.concurrent.lock; |
MCS锁
MCS锁可以解决上面的CLH锁的缺点,MCS 来自于其发明人名字的首字母: John Mellor-Crummey和Michael Scott。
MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋(与CLH自旋锁不同的地方,不在轮询前驱的状态,而是由前驱主动通知),从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
原理
- 每个线程持有一个自己的node,node有一个locked属性,true表示等待获取锁,false表示可以获取到锁,并且持有下一个node(后继者)的引用(可能存在)
- 线程在轮询自己node的locked状态,true表示锁被其他线程暂用,等待获取锁,自旋。
- 线程释放锁的时候,修改后继者(nextNode)的locked属性,通知后继者结束自旋。
代码实现
1 | package io.github.brightloong.concurrent.lock; |
和CLH锁类似的,我自己用ThreadLocal保存node,而不通过函数传参的方式实现了一个MCS锁,代码如下(大概测试了下没问题,如果有问题希望指出,感谢):
1 | package io.github.brightloong.concurrent.lock; |