吃透Java并发:AQS结构详解及其CLH变种、CLH、MCS

1、前言

俗话说得好,学会AQS就学会了JUC。今天我们就来了解一下,这个传说中的AQS的内部结构。
(强烈建议英语好的同学直接看AQS的注释!!)

2、AQS的构成

抽象队列同步器(即AQS),通过一系列模板办法为我们实现JUC下各种各样的工具锁提供了基础,通过继承它,我们可以很轻松的实现一把自己的锁。

假如是让我们来实现锁的逻辑,结合synchronized的实现原理,我们很容易得出,我们需要:

  1. 表示线程是否被上锁的状态信息
  2. 线程竞争失败时需要存放的队列
  3. 能让线程进行阻塞/唤醒

有了这三个东西,我们就可以保证线程通过修改可见变量的状态来标识是否被占用,以及上锁失败时线程应该阻塞的地方。

当然,为了知道当前是哪个线程得到了锁资源,我们还需要一个变量来表明当前持锁线程:exclusiveOwnerThread

2.1 状态信息state

在AQS中,使用了一个被volatile修饰的int类型变量来标识状态信息。

	/**
     * The synchronization state.
     */
    private volatile int state;

在使用过程中,我们可以通过定义volatile的状态来实现不同的逻辑。比如:

  1. ReentrantLock中将state=0表示无锁状态,state>0表示加锁状态。如state=3就代表,有个线程已经持有锁,并且重入了两次。
  2. CountDownLatch中则是在构造器中定义state的大小来实现计数器的功能
  3. Semaphore中也是通过定义state的大小与修改state来实现限流器的功能

通过CAS来修改state的状态,AQS不用申请monitor就能保证线程安全,同时还能实现各种各样不同的功能。

2.2 节点信息Node

线程竞争失败,自然需要在一个地方阻塞等待条件成立。但是直接把Thread放进去队列似乎有点不太合适,既然是队列,就得有节点。于是AQS需要把线程封装成节点信息之后再放进去,这里贴一下Node的成员变量:

		/**
         * 标记节点未共享模式
         * */
        static final Node SHARED = new Node();
        /**
         *  标记节点为独占模式
         */
        static final Node EXCLUSIVE = null;

        /**
         * 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
         * */
        static final int CANCELLED =  1;
        /**
         *  后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
         *  将会通知后继节点,使后继节点的线程得以运行。
         */
        static final int SIGNAL    = -1;
        /**
         *  节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
         *  该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
         */
        static final int CONDITION = -2;
        /**
         * 表示下一次共享式同步状态获取将会被无条件地传播下去
         */
        static final int PROPAGATE = -3;

        /**
         * 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
         * 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
         * 即被一个线程修改后,状态会立马让其他线程可见。
         */
        volatile int waitStatus;

        /**
         * 前驱节点,当前节点加入到同步队列中被设置
         */
        volatile Node prev;

        /**
         * 后继节点
         */
        volatile Node next;

        /**
         * 节点同步状态的线程
         */
        volatile Thread thread;

        /**
         * 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
         * 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
         */
        Node nextWaiter;

这里我们关注三个点:

  1. 前驱节点: volatile Node prev;
  2. 后继节点:volatile Node next;
  3. 节点同步状态的线程:volatile Thread thread;

在关于AQS竞争与释放流程剖析的时候,这几个东西可是十分重要的。

2.3 阻塞/唤醒

其实这里没有什么好说的,这里AQS使用了LockSupport工具类中的方法,而再进一步就是使用了unsafe中的park和unpark方法。

如果还要进一步就是涉及到由C语言实现的_count(是否阻塞)、_cond(条件变量)、_mutex(互斥锁)。

2.4 当前持有锁线程

在AQS中并没有直接使用一个变量来标识当前持有锁的线程,而是选择继承了另一个抽象类:AbstractOwnableSynchronizer

在这个类中,主要就是围绕当前持有锁线程变量提供了get/set操作。

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
    private static final long serialVersionUID = 3737899427754241961L;

    protected AbstractOwnableSynchronizer() { }

    /**
     * 独占模式同步器的当前持有线程.
     * transient关键字表示属性不参与序列化
     */
    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}


2.5 头结点与尾节点

关于这个,会在AQS多线程下获取与释放流程剖析时进行讲解。

	/**
     * 指向同步等待队列的头节点
     */
    private transient volatile Node head;

    /**
     * 指向同步等待队列的尾节点
     */
    private transient volatile Node tail;


3、CLH队列

CLH队列也是AQS的组成部分。之所以把它单独拎出来讲,是因为CLH是一种算法,而且AQS中是CLH队列的变种。同时CLH队列的优化还有MCS。

在讲CLH队列之前,我们先了解两个前置知识点(我抄来的):

  • 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较好地解决SMP的扩展问题,
    当CPU数量增加时,因为访问远地内存的延时远远超过本地内存,系统性能无法线性增加。

3.1 CLH锁

在基础的自旋锁算法下,为了保证公平锁,CLH队列中的申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋

CLH队列锁是一种基于链表的可扩展、高性能、公平的自旋锁。优点是空间复杂度低,但是在NUMA系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,则性能会大打折扣。

3.2 MCS锁

为了应对这种情况,MCS锁修改了自旋的规则:CLH是在前趋结点的locked域上自旋等待,而MCS是在自己的结点的locked域上自旋等待。正因为如此,它解决了CLH在NUMA系统架构中获取locked域状态内存过远的问题

3.3 AQS中的变种CLH锁

AQS在CLH的基础上进行了变种:CLH是单向队列,其主要特点是自旋检查前驱节点的locked状态。而AQS同步队列是双向队列,每个节点也有状态waitStatus,而其并不是一直对前驱节点的状态自旋,在尝试自旋一次后会将线程阻塞让出CPU时间片,等待前驱节点主动唤醒自己。

4、结尾

事实上这一章都是为了下一章的AQS多线程下获取与释放流程剖析做铺垫,对AQS有了大概的了解,我们在下一章就不至于那么懵逼~

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个重要类,它可以理解为抽象的队列同步器。AQS提供了一种基于FIFO队列的同步机制,用于实现各种同步器,如ReentrantLock、CountDownLatch、Semaphore等。 AQS的核心思想是使用一个volatile的int类型变量state来表示同步状态,通过CAS(Compare and Swap)操作来实现对state的原子更新。AQS内部维护了一个双向链表,用于保存等待获取同步状态的线程。 AQS的具体实现包括以下几个方面: 1. 内部属性:AQS内部有两个重要的属性,一个是head,表示队列的头节点;另一个是tail,表示队列的尾节点。 2. 入队操作:AQS的入队操作是通过enq方法实现的。在入队操作中,首先判断队列是否为空,如果为空,则需要初始化队列;否则,将新节点添加到队列的尾部,并更新tail指针。 3. CAS操作:AQS的CAS操作是通过compareAndSetHead和compareAndSetTail方法实现的。这些方法使用CAS操作来更新head和tail指针,保证操作的原子性。 4. 出队操作:AQS的出队操作是通过deq方法实现的。在出队操作中,首先判断队列是否为空,如果为空,则返回null;否则,将头节点出队,并更新head指针。 5. 同步状态的获取和释放:AQS提供了acquire和release方法来获取和释放同步状态。acquire方法用于获取同步状态,如果获取失败,则会将当前线程加入到等待队列中;release方法用于释放同步状态,并唤醒等待队列中的线程。 通过继承AQS类,可以实现自定义的同步器。具体的实现方式是重写AQS的几个关键方法,如tryAcquire、tryRelease等,来实现对同步状态的获取和释放。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值