MCS锁的原理和实现
Posted dm_vincent
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MCS锁的原理和实现相关的知识,希望对你有一定的参考价值。
前情回顾
上一篇文章中主要讨论了自旋锁的特点和其适用场景,然后给出了两种自旋锁的简单实现。
存在的问题
无论是简单的非公平自旋锁还是公平的基于排队的自旋锁,由于执行线程均在同一个共享变量上自旋,申请和释放锁的时候必须对该共享变量进行修改,这将导致所有参与排队自旋锁操作的处理器的缓存变得无效。如果排队自旋锁竞争比较激烈的话,频繁的缓存同步操作会导致繁重的系统总线和内存的流量,从而大大降低了系统整体的性能。
所以,需要有一种办法能够让执行线程不再在同一个共享变量上自旋,避免过高频率的缓存同步操作。于是MCS和CLH锁应运而生。
这两个锁的名称都来源于发明人的名字首字母:
MCS:John Mellor-Crummey and Michael Scott。
CLH:Craig,Landin and Hagersten。
本文先介绍MCS锁的原理和相应实现。
MCS锁
MCS自旋锁是一种基于单向链表的高性能、公平的自旋锁,申请加锁的线程只需要在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
先上实现代码,然后在分析重点:
public class MCSLockV2
/**
* MCS锁节点
*/
public static class MCSNodeV2
/**
* 后继节点
*/
volatile MCSNodeV2 next;
/**
* 默认状态为等待锁
*/
volatile boolean blocked = true;
/**
* 线程到节点的映射
*/
private ThreadLocal<MCSNodeV2> currentThreadNode = new ThreadLocal<>();
/**
* 指向最后一个申请锁的MCSNode
*/
volatile MCSNodeV2 queue;
/**
* 原子更新器
*/
private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater
.newUpdater(
MCSLockV2.class,
MCSLockV2.MCSNodeV2.class,
"queue");
/**
* MCS获取锁操作
*/
public void lock()
MCSNodeV2 cNode = currentThreadNode.get();
if (cNode == null)
// 初始化节点对象
cNode = new MCSNodeV2();
currentThreadNode.set(cNode);
// 将当前申请锁的线程置为queue并返回旧值
MCSNodeV2 predecessor = (MCSNodeV2) UPDATER.getAndSet(this, cNode); // step 1
if (predecessor != null)
// 形成链表结构(单向)
predecessor.next = cNode; // step 2
// 当前线程处于等待状态时自旋(MCSNode的blocked初始化为true)
// 等待前驱节点主动通知,即将blocked设置为false,表示当前线程可以获取到锁
while (cNode.blocked)
else
// 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己为非阻塞 - 表示已经加锁成功
cNode.blocked = false;
/**
* MCS释放锁操作
*/
public void unlock()
// 获取当前线程对应的节点
MCSNodeV2 cNode = currentThreadNode.get();
if (cNode == null || cNode.blocked)
// 当前线程对应存在节点
// 并且
// 锁拥有者进行释放锁才有意义 - 当blocked未true时,表示此线程处于等待状态中,并没有获取到锁,因此没有权利释放锁
return;
if (cNode.next == null && !UPDATER.compareAndSet(this, cNode, null))
// 没有后继节点的情况,将queue置为空
// 如果CAS操作失败了表示突然有节点排在自己后面了,可能还不知道是谁,下面是等待后续者
// 这里之所以要忙等是因为上述的lock操作中step 1执行完后,step 2可能还没执行完
while (cNode.next == null)
if (cNode.next != null)
// 通知后继节点可以获取锁
cNode.next.blocked = false;
// 将当前节点从链表中断开,方便对当前节点进行GC
cNode.next = null; // for GC
// 清空当前线程对应的节点信息
currentThreadNode.remove();
/**
* 测试用例
*
* @param args
*/
public static void main(String[] args)
final MCSLockV2 lock = new MCSLockV2();
for (int i = 1; i <= 10; i++)
new Thread(generateTask(lock, String.valueOf(i))).start();
private static Runnable generateTask(final MCSLockV2 lock, final String taskId)
return () ->
lock.lock();
try
Thread.sleep(3000);
catch (Exception e)
System.out.println(String.format("Thread %s Completed", taskId));
lock.unlock();
;
节点定义以及锁拥有的字段
首先,需要定义一个节点对象。这个节点即代表了申请加锁的线程之间的先后关系,节点通过其next属性组成一个单项链表的数据结构。另外,节点的blocked属性表示的是该节点是否处于等待加锁的状态,默认值为true,表示节点的初始状态是等待中。
然后,锁本身的实现有三个属性:
- 节点类型queue:表示当前链表的尾部,每个新加入排队的线程都会被放到这个位置
- ThreadLocal类型的currentThreadNode:保存的是从线程对象到节点对象实例的映射关系
- 针对queue字段的原子更新器AtomicReferenceFieldUpdater:通过AtomicReferenceFieldUpdater对queue字段操作的一层包装,任何操作都不会直接施加在queue上,而是通过它,它提供了一些CAS操作来保证原子性
最重要的就是其中的lock和unlock方法了。简单分析一下实现方法:
lock方法
简单提炼一下此方法的操作步骤和要点:
- 获取当前线程和对应的节点对象(不存在则初始化)
- 将queue通过getAndSet这一原子操作更新为第一步中得到的节点对象,返回可能存在的前驱节点,如果前驱存在跳转到Step 3;不存在跳转到Step 4
- 建立单向链表关系,由前驱节点指向当前节点;当前线程开始在当前节点对象的blocked字段上自旋等待(等待前驱节点改变其blocked的状态)
- 没有前驱节点表示此时并没有除当前线程外的线程拥有锁,因此可以直接改变节点的blocked为false,lock方法执行完毕表示加锁成功
unlock方法
简单提炼一下此方法的操作步骤和要点:
- 获取当前线程和对应的节点对象;如果节点不存在或者节点状态为等待的话直接返回,因为只有拥有锁的线程才有资格进行释放锁的操作
- 清空当前线程对应的节点信息
- 判断当前节点是否拥有后继节点,如果没有的话跳转到Step 4;没有的话跳转到Step 5
- 利用原子更新器的CAS操作尝试将queue设置为null。设置成功的话表示锁释放成功,unlock方法执行完毕返回;设置失败的话表示Step 3和Step 4的CAS的操作之间有别的线程来捣乱了,queue此时并非指向当前节点,因此需要忙等待确保链表结构就绪(参考代码注释:lock操作的getAndSet操作和链表建立并非是原子性的)
- 此时当前节点的后继节点已经就绪了,所以可以改变后继节点的blocked状态,另在其上等待的线程退出自旋。最后还会更新当前节点的next指向为null辅助垃圾回收
以上加锁和释放锁的每个步骤都有比较详细的注释,相信仔细读的话看懂并不是难事。
main方法
针对MCS锁实现的一个用例,模拟了10个线程抢锁的场景。
总结
实现的代码量虽然不多,但是lock和unlock的设计思想还是有些微妙之处,想要实现正确也并不容易。
需要把握的几个重点:
- MCS锁的节点对象需要有两个状态,next用来维护单向链表的结构,blocked用来表示节点的状态,true表示处于自旋中;false表示加锁成功
- MCS锁的节点状态blocked的改变是由其前驱节点触发改变的
- 加锁时会更新链表的末节点并完成链表结构的维护
- 释放锁的时候由于链表结构建立的时滞(getAndSet原子方法和链表建立整体而言并非原子性),可能存在多线程的干扰,需要使用忙等待保证链表结构就绪
另外需要注意的是,MCS锁是一种不可重入的独占锁。
在下一篇文章中将介绍一种更轻巧的解决方案:CLH锁。
参考资料
- https://coderbee.net/index.php/concurrent/20131115/577
- https://en.wikipedia.org/wiki/Spinlock
- https://www.ibm.com/developerworks/cn/linux/l-cn-mcsspinlock/
以上是关于MCS锁的原理和实现的主要内容,如果未能解决你的问题,请参考以下文章
《并发系列一》AbstractQueuedSynchronizer(AQS)- 互斥锁源码剖析