AQS队列以及JUC相关同步组件详解

90 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

1. 概述

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它的底层是int状态码+队列的结构。其使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,下面我们来介绍其实现原理。

2. 实现原理

AQS的底层是一个封装了线程的双向队列,其数据结构如下所示:

static final class Node { 
    /** 标识当前节点在共享模式 */ s
    tatic final Node SHARED = new Node(); 

    /** 标识当前节点在独占模式 */ 
    static final Node EXCLUSIVE = null; 

    /** 表示当前的线程被取消 */ 
    static final int CANCELLED = 1; 
    /** 表示当前节点的后继节点包含的线程需要运行,也就是unpark*/ 
    static final int SIGNAL = -1; 
    /** 表示当前节点在等待condition,也就是在condition队列中*/ 
    static final int CONDITION = -2; 
    /**值为-3,表示当前场景下后续的acquireShared能够得以执行*/ 
    static final int PROPAGATE = -3; 

    /**表当前节点的状态值,取值为上面的四个常量*/ 
    volatile int waitStatus; 

    /** 前驱节点*/ 
    volatile Node prev; 

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

    /**节点线程*/ 
    volatile Thread thread; 

    /**连接条件队列*/ 
    Node nextWaiter;

AQS涉及到四个重要的方法:

  • acquire()
  • release()
  • tryacquire()
  • tryrelease()

其中,前两个方法是AQS底层框架写好的,我们可以通过实现后两个方法,定制我们自己的同步器(ReentrantLock,Semaphore与CountDownLatch等都是如此)。

image.png

acquire()

acquire()用于获得同步状态,具体的源码考虑的case较多,这里我们提炼出最终要的两种情况:入队操作与自旋操作。

  • 入队操作:触发入队操作时,会生成一个新的节点,包装当前线程,并通过一个CAS操作挂到队尾,入队操作是有并发问题的,CAS操作就是为了解决可能的并发问题。
  • 自旋操作:挂在队伍中的节点会触发自旋操作,自旋的过程中会判断自己是否为队首节点,是的话调用tryacquire()方法,如果判断为true,那么就进行唤醒操作,使当前节点包装的线程获得自旋状态,自旋的操作只需要判别前置节点,不存在并发问题。

release()

获得同步状态的节点会在执行完线程逻辑之后触发release()方法,并且会间接触发tryrelease()。release()方法会将当前节点的后置节点唤醒。

3. ReentrantLock

公平锁与非公平锁

ReentrantLock 的底层就是AQS队列,使用state状态码标记同步状态以及重入层数。

  • 非公平:默认为非公平锁,新加入的线程会尝试和对手元素争抢修改状态码,可能存在插队现象。
  • 公平:新加入的线程直接挂在队尾。

读写锁

ReentrantLock的读写锁也是对state状态码做的文章,状态码的前半部分维护有多少个读线程,后半部分维护是否有写线程。

4. Semaphore

Semaphore 将state做了类似于Token的使用,线程在访问同步资源以前,先尝试CAS使state-1,如果失败,就进入AQS队列。

5. CountDownLatch

CountDownLatch 同步器给state设置一个初始值,如果state没有减到0,就将线程放在AQS队列中等待,steta归零后统一唤醒。