携手创作,共同成长!这是我参与「掘金日新计划 · 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等都是如此)。
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归零后统一唤醒。