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,表示节点的初始状态是等待中。

然后,锁本身的实现有三个属性:

  1. 节点类型queue:表示当前链表的尾部,每个新加入排队的线程都会被放到这个位置
  2. ThreadLocal类型的currentThreadNode:保存的是从线程对象到节点对象实例的映射关系
  3. 针对queue字段的原子更新器AtomicReferenceFieldUpdater:通过AtomicReferenceFieldUpdater对queue字段操作的一层包装,任何操作都不会直接施加在queue上,而是通过它,它提供了一些CAS操作来保证原子性

最重要的就是其中的lock和unlock方法了。简单分析一下实现方法:

lock方法

简单提炼一下此方法的操作步骤和要点:

  1. 获取当前线程和对应的节点对象(不存在则初始化)
  2. 将queue通过getAndSet这一原子操作更新为第一步中得到的节点对象,返回可能存在的前驱节点,如果前驱存在跳转到Step 3;不存在跳转到Step 4
  3. 建立单向链表关系,由前驱节点指向当前节点;当前线程开始在当前节点对象的blocked字段上自旋等待(等待前驱节点改变其blocked的状态)
  4. 没有前驱节点表示此时并没有除当前线程外的线程拥有锁,因此可以直接改变节点的blocked为false,lock方法执行完毕表示加锁成功

unlock方法

简单提炼一下此方法的操作步骤和要点:

  1. 获取当前线程和对应的节点对象;如果节点不存在或者节点状态为等待的话直接返回,因为只有拥有锁的线程才有资格进行释放锁的操作
  2. 清空当前线程对应的节点信息
  3. 判断当前节点是否拥有后继节点,如果没有的话跳转到Step 4;没有的话跳转到Step 5
  4. 利用原子更新器的CAS操作尝试将queue设置为null。设置成功的话表示锁释放成功,unlock方法执行完毕返回;设置失败的话表示Step 3和Step 4的CAS的操作之间有别的线程来捣乱了,queue此时并非指向当前节点,因此需要忙等待确保链表结构就绪(参考代码注释:lock操作的getAndSet操作和链表建立并非是原子性的)
  5. 此时当前节点的后继节点已经就绪了,所以可以改变后继节点的blocked状态,另在其上等待的线程退出自旋。最后还会更新当前节点的next指向为null辅助垃圾回收

以上加锁和释放锁的每个步骤都有比较详细的注释,相信仔细读的话看懂并不是难事。

main方法

针对MCS锁实现的一个用例,模拟了10个线程抢锁的场景。

总结

实现的代码量虽然不多,但是lock和unlock的设计思想还是有些微妙之处,想要实现正确也并不容易。

需要把握的几个重点:

  1. MCS锁的节点对象需要有两个状态,next用来维护单向链表的结构,blocked用来表示节点的状态,true表示处于自旋中;false表示加锁成功
  2. MCS锁的节点状态blocked的改变是由其前驱节点触发改变的
  3. 加锁时会更新链表的末节点并完成链表结构的维护
  4. 释放锁的时候由于链表结构建立的时滞(getAndSet原子方法和链表建立整体而言并非原子性),可能存在多线程的干扰,需要使用忙等待保证链表结构就绪

另外需要注意的是,MCS锁是一种不可重入的独占锁。

在下一篇文章中将介绍一种更轻巧的解决方案:CLH锁。

参考资料

  1. https://coderbee.net/index.php/concurrent/20131115/577
  2. https://en.wikipedia.org/wiki/Spinlock
  3. https://www.ibm.com/developerworks/cn/linux/l-cn-mcsspinlock/

以上是关于MCS锁的原理和实现的主要内容,如果未能解决你的问题,请参考以下文章

CLH锁的原理和实现

CLH锁的原理和实现

《并发系列一》AbstractQueuedSynchronizer(AQS)- 互斥锁源码剖析

Ticket Lock, CLH Lock, MCS Lock

AQS锁的原理

微机原理基础—— MCS51组成