要让你写一个java版的并发同步库,你会怎么思考设计???先思考三五分钟
请先拜读下老外的paperhttp://gee.cs.oswego.edu/dl/papers/aqs.pdf
1. 简介
AbstractQueuedSynchronizer,简称AQS,中文翻译为抽象队列同步器,提供了基于同步状态、阻塞与唤醒线程及队列模型的基础框架。JDK中许多并发工具类的实现都基于AQS,如ReentrantLock、Semaphore、CountDownLatch等,它们都是基于AQS的抽象,实现不同的锁机制。单从类名来看,我们就已经可以得到3个重要信息:
Abstract:抽象类,通常无法直接使用;
Queued:队列,借助队列实现功能;
Synchronizer:同步器,用于控制并发。
2. 特性
先看下源码:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 创建AQS实例,初始化state为0,为子类提供
*/
protected AbstractQueuedSynchronizer() { }
/*---------------- 同步队列构成 ---------------*/
// 等待队列节点类型
static final class Node {
volatile int waitStatus;//线程在队列中的状态
volatile Node prev;//链表前驱节点指针
volatile Node next;//链表后继节点指针
volatile Thread thread;//节点对应的线程对象
Node nextWaiter;
}
/**
* 除了初始化之外,它只能通过setHead方法进行修改。注意:如果head存在,它的waitStatus保证不会被取消
*/
private transient volatile Node head;
/**
* 等待队列的尾部,懒初始化,之后只在enq方法加入新节点时修改
*/
private transient volatile Node tail;
/*---------------- 同步状态相关 ---------------*/
/**
* volatile修饰, 标识同步状态,state为0表示锁空闲,state>0表示锁被持有,可以大于1,表示被重入
*/
private volatile int state;
/**
* 利用CAS操作更新state值
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
static final long spinForTimeoutThreshold = 1000L;
// 这部分和CAS有关
// 获取Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 记录state在AQS类中的偏移值
private static final long stateOffset;
static {
try {
// 初始化state变量的偏移值
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
} catch (Exception ex) { throw new Error(ex); }
}
}
2.1 同步队列
AQS通过同步双向队列来完成资源获取线程的排队工作,内部通过节点head【实际上是虚拟节点,真正的第一个线程在head.next的位置】和tail记录队首和队尾元素,队列元素类型为Node,同时每个节点还对应一个线程对象,我们可以把它理解为每个节点某一时段都代表一个线程。。
如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程
当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
2.2 一个核心状态
volatile int state,其是一个整数类型,意为同步状态,也可以理解为资源。该字段的几个方法均为final,即禁止子类覆盖。
资源的状态state维护交由子类调用,由子类通过state判断是否获取锁,以及释放锁是否成功。state代表一种资源,具体的资源分配情况由具体的实现操作。state的更新是基于cas更新的,通过volatile关键词修饰,保障并发时的可见性和有序性。
小结:
类型 | 名称 | 含义 |
Node |
head |
队列的节点类型,等待(双向)队列的头节点 |
Node |
tail | 队列的节点类型,等待(双向)队列的尾节点 |
int |
state | 同步器状态 |
Thread |
exclusiveOwnerThread |
继承的变量,表示排它锁模式下线程的持有者 |
3. 数据结构
3.1 队列定义
3.1.1 双向队列
基于队列,普通链表是最先想到的实现方式,其定义如下:
// AQS静态内部类,作为链表/队列中的数据节点
static final class Node {
// 表明是一个共享锁
static final Node SHARED = new Node();
// 表明是一个排它锁
static final Node EXCLUSIVE = null;
// 上面的节点状态,具体枚举定义见下表,通过cas更新
volatile int waitStatus;
// 链表前继节点
volatile Node prev;
// 链表后继节点
volatile Node next;
// 节点对应的线程对象
volatile Thread thread;
// 下一个等待节点,在条件队列中使用;同时也是用来区分排它锁节点和共享锁节点的标识
Node nextWaiter;
}
其中节点状态waitStatus的枚举状态值如下:
变量名 |
变量值 | 含义 |
CANCELLED |
1 | 表明当前节点的线程已被取消 |
SIGNAL |
-1 | 表明下一个节点需要前一节点唤醒,这样下一个节点便可以安心睡眠了 |
CONDITION |
-2 | 表明线程在等待条件,条件队列才用的上,如ReentrantLock的Condition |
PROPAGATE |
-3 | 表明下一个共享节点应该被无条件传播,当需要唤醒下一个共享节点时,会一直传播唤醒下一个直到非共享节点 |
- | 0 | 初始值,刚竞争资源进入队列的时候的初始状态 |
3.1.2 条件队列
// AQS内部类
public class ConditionObject implements Condition{
// 条件队列第一个节点
private transient Node firstWaiter;
// 条件队列最后一个节点
private transient Node lastWaiter;
// 注意:上文提到Node结构中有一个nextWaiter节点,一个使用场景便是条件队列的下一个节点(看作单向链表结构)。
// 当前线程等待,进入条件队列
public final void await(){}
public final long awaitNanos(long nanosTimeout){}
// 唤醒基于当前条件等待的一个线程,从第一个开始,加入到同步队列中,等待获取锁资源
public final void signal() {}
// 唤醒所有条件等待线程,加入到同步队列中
public final void signalAll() {}
}
ConditionObject中提到的await和signal开头的方法,类似于Object的wait()和notify() 方法,需要获取到锁后调用。
4. 重要方法分析
对于AQS来说,线程同步的关键是对state进行操作,根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。
4.1 独占式获取与释放同步状态
使用独占的方式获取的资源是与具体线程绑定的,如果一个线程获取到了资源,便标记这个线程已经获取到,其他线程再次尝试操作state获取资源时就会发现当前该资源不是自己持有的,就会在获取失败后阻塞。
// 独占式获取同步状态,成功后,其他线程需要等待该线程释放同步状态才能获取同步状态
public final void acquire(int arg) {
// 首先调用 tryAcquire【需要子类实现】尝试获取资源,本质就是设置state的值,获取成功就直接返回
if (!tryAcquire(arg) &&
// 获取失败,就将当前线程封装成类型为Node.EXCLUSIVE的Node节点,并插入AQS阻塞队列尾部
// 然后通过自旋获取同步状态
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 与 acquire(int arg) 相同,但是该方法响应中断。
// 如果其他线程调用了当前线程的interrupt()方法,响应中断,抛出异常。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// interrupted()方法将会获取当前线程的中断标志并重置
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
//尝试获取锁,如果获取失败会将当前线程挂起指定时间,时间到了之后当前线程被激活,如果还是没有获取到锁,就返回false。
//另外,该方法会对中断进行的响应,如果其他线程调用了当前线程的interrupt()方法,响应中断,抛出异常。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
// 独占式释放同步状态
public final boolean release(int arg) {
// 尝试使用tryRelease释放资源,本质也是设置state的值
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// LockSupport.unpark(thread) 激活AQS里面被阻塞的一个线程
// 被激活的线程则使用tryAcquire 尝试,看当前状态变量state的值是否能满足自己的需要,
//满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。
unparkSuccessor(h);
return true;
}
return false;
}
4.2 共享式获取与释放同步状态
对应共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用CAS 方式进行获取即可。
//共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,
// 与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
// 尝试获取资源,如果成功则直接返回
// 如果失败,则将当前线程封装为类型为Node.SHARED的Node节点并插入AQS阻塞队列尾部
// 并使用LockSupport.park(this)挂起自己
doAcquireShared(arg);
}
// 共享式获取同步状态,响应中断
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
//共享式获取同步状态,增加超时限制
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
//共享式释放同步状态
public final boolean releaseShared(int arg) {
// 尝试释放资源
if (tryReleaseShared(arg)) {
// 调用LockSupport.unpark(thread)激活AQS队列里被阻塞的一个线程。
// 被激活的线程使用tryReleaseShared查看当前状态变量state是否能满足自己的需要。
// 如果满足需要,则线程被激活继续向下运行,否则还是放入AQS队列并被挂起
doReleaseShared();
return true;
}
return false;
}
5. AQS的实现与应用分析
AQS提供多种实现,用于不同的业务场景,下面我们一起来看一下常见的几种:
AQS实现 |
应用场景 |
ReentrantLock |
可重入锁,对资源的互斥访问,支持多条件、超时、尝试获取等,如精准阻塞唤醒线程的生产消费模型的实现 |
ReentrantReadWriteLock |
可重入读写锁,用于读写场景,如读多写少的业务场景,利用读读不互斥、读写互斥的特性实现高性能的数据一致性 |
CountDownLatch |
计数器或闭锁,多个线程各持有一个资源,所有线程资源释放后唤醒最终等待的线程,起到线程间通讯的作用,如多线程分片计算最后统计的场景 |
Semaphore |
信号灯、信号量,主要用于控制可以同时访问某种资源的线程个数,如做流量分流,对于公共资源有限的场景,以及数据库连接等 |
就以它分析
ReentrantLock意为可重入锁,是一种排它锁的实现,只能一个线程可访问,如果其它线程来竞争资源的话会进入同步队列进行等待,其包括公平与非公平两种方式。
对于这种对临界资源加锁互斥的实现,还有常见的JVM提供的synchronized,源码分析前,我们先对比下两者特性:
ReentrantLock |
synchronized | |
实现机制 |
依赖于AQS |
JVM实现基于对象的监视器锁 |
可重入性 |
可重入 |
可重入 |
灵活性 |
更加灵活,支持公平与非公平、超时尝试、多条件的锁等待和唤醒、可中断 |
相对没那么灵活 |
加/释放锁 |
显示调用api加锁,且需要显示释放,同时需要确保异常后也能释放 |
使用起来简单,不用显示的加锁和释放锁 |
使用场景 |
对锁的使用场景需要更加灵活,如可以通过多条件精准阻塞唤醒线程,如jdk本身提供的一些阻塞队列 |
本来是重量级锁,优化增加了偏向、轻量级锁等,在线程不怎么竞争的情况下或灵活度要求不那么高的场景下更推荐,如常见单例模式的DCL实现 |
ReentrantLock内部定义了一个继承AQS的类:
// ReentrantLock抽象静态内部类
abstract static class Sync extends AbstractQueuedSynchronizer {
// 获取锁的抽象方法。为什么抽象往下看
abstract void lock();
// 非公平的获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// cas尝试,成功的话更新锁的持有线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入锁的体现
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 关键:释放锁,该访问是实现了AQS的tryRelease方法的
protected final boolean tryRelease(int releases){
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果释放后锁没有了,持有锁的线程标识也置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// 创建条件,对应AQS的条件队列
final ConditionObject newCondition() {
return new ConditionObject();
}
}
Sync是个抽象类,包含公平锁FairSync与非公平锁NonFairSync的两个实现:
// 非公平锁实现
static final class NonfairSync extends Sync {
// 非公平获取锁,不用排队,来到就可以试一试尝试获取锁
final void lock() {
// 尝试修改AQS的state从0到1,成功的话表示获取同步锁成功,并设置当前锁持有线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 否则调用AQS的竞争锁方法
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 调用Sync的非同步锁尝试获取,实现见Sync
return nonfairTryAcquire(acquires);
}
}
// 公平锁实现
static final class FairSync extends Sync {
// 公平锁,没有尝试修改状态,直接获取锁
final void lock() {
// 内部会调用下面的tryAcquire(int acquires)方法
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 注意:hasQueuedPredecessors() 与非公平锁的区别的地方,对于公平锁,如果队列有节点,直接跳过尝试获取资源
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
ReentrantLock核心变量
// ReentrantLock的成员变量,核心的方法都在Sync类体现了
private final Sync sync;
ReentrantLock核心方法
返回值 | 方法名 |
说明 |
- | ReentrantLock() |
构造器,默认为非公平锁;有参构造器可以指定使用公平锁 |
void |
lock() |
加锁 |
void |
lockInterruptibly() |
加锁,可中断,线程被中断会抛异常 |
boolean |
tryLock() |
尝试获取锁,不会进入AQS同步器队列,仅尝试cas state,成功与失败都会立马返回 |
void |
unlock() |
释放锁 |
Condition |
newCondition() |
获取条件对象 |
锁竞争源码分析
假设线程1持有锁执行任务时,线程2竞争锁资源。
// java.util.concurrent.locks.ReentrantLock.NonfairSync
final void lock() {
// cas尝试获取资源,案例中线程1会获取成功,直接返回;
// 线程2会进入else逻辑,竞争锁资源。这里主要看线程2的逻辑
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 进入AQS竞争锁,继续往下看
acquire(1);
}
接着,进入AQS类操作:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
// 此处的tryAcquire(arg)会尝试cas资源,由AQS子类ReentrantLock实现的
// 1、如果竞争成功则直接返回
// 2、否则调用addWaiter(Node.EXCLUSIVE),创建一个排它锁节点
// 3、再调用acquireQueued进入队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 继续看 addWaiter(Node.EXCLUSIVE) 创建排它锁节点
private Node addWaiter(Node mode) {
// 创建节点,传入当前线程,表明线程与节点的对应,mode是排它锁的标识
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果尾节点不空
if (pred != null) {
// 将当前线程2所在节点的前继节点指向尾节点
node.prev = pred;
// cas将当前线程2所在节点设置成尾节点,成功的话则返回true
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 上面cas操作失败的情况下,会通过轮询不断尝试直至成功,并返回节点
// 当前案例会进入当前方法,因为线程1虽然持有锁,但是没有队列,所以pred=null(tail也为null)
enq(node);
return node;
}
// 不断轮询设置尾节点的操作
private Node enq(final Node node) {
for (;;) { // 无限循环
Node t = tail;
if (t == null) {
// 没有尾节点,cas一个新节点作为头节点,并且将尾节点也指向它
// 注意:该节点没有对应的线程,可以看作是线程1的
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 再一次循环到此,将线程2的节点设置成尾节点,直至成功。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 继续看调用acquireQueued进入队列,已经通过 addWaiter(Node.EXCLUSIVE) 创建排它锁节点
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 中断标识,但如果中断也不会抛异常
boolean interrupted = false;
for (;;) { // 循环
// 获取线程2节点的前继节点
final Node p = node.predecessor();
// 如果p节点是头节点,此例是的,所以会再次尝试获取下锁
// 聪明啊,真是不放过没一次机会去尝试,如果刚巧线程1此刻执行完任务释放了锁,直接成功获取锁;当然主要应该为了唤醒后获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 线程1还没有执行完任务,所以会进入到这里,if中两个判断的方法源码见下文
// parkAndCheckInterrupt()调用LockSupport.park(this);进入线程等待状态,等待唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果线程终端,设置interrupted
interrupted = true;
}
} finally {
if (failed)
// 如果中断/异常,会进行取消节点
cancelAcquire(node);
}
}
// 继续看 shouldParkAfterFailedAcquire(p, node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前继节点的等待状态,AQS介绍已经说过,默认是0,会经过下面的cas改成SIGNAL(-1)
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果已经是-1,表示后继节点可以安心睡觉了,前节点锁释放后会唤醒后继节点
return true;
if (ws > 0) {
// ws大于1表示节点状态已经取消了,可以跳过该节点了
do {
// 比较难看懂,从后往前看,pred = pred.prev表示前节点指到再前一个节点;
// node.prev = pred当前node节点的前节点指向刚刚的pred。加上后面那句pred.next = node;
// 其实就是删除中间的取消节点。
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将waitStatue cas成signal状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 继续看,shouldParkAfterFailedAcquire调用后,最终会返回true,紧接着调用parkAndCheckInterrupt(),进入睡眠状态
private final boolean parkAndCheckInterrupt() {
// 进入awit,正常唤醒LockSupport.unpark(线程)
LockSupport.park(this);
// 是否是中断返回
return Thread.interrupted();
}
锁释放源码分析
// java.util.concurrent.locks.ReentrantLock
public void unlock() {
// 核心方法调用,进行锁释放
sync.release(1);
}
接着,进入AQS类操作:
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
// 尝试释放锁,由AQS子类ReentrantLock实现
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒h节点线程
return true;
}
return false;
}
下面我们继续看ReentrantLock的tryRelease实现,可见释放锁的时候,对于公平和非公平锁,都是调用Sync类定义的方法。
// java.util.concurrent.locks.ReentrantLock#Sync
protected final boolean tryRelease(int releases) {
// 减后得到目前还被锁定的资源
int c = getState() - releases;
// 如果当前线程不是队列锁持有者,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
// 如果没有锁了,队列锁持有者置空
setExclusiveOwnerThread(null);
}
// 不用cas更新状态,会成功,因为当前线程是持有排它锁的
setState(c);
return free;
}
tryRelease()成功的话,会获取头节点,如果队列有节点(此例中的线程2),会继续调用AQS的unparkSuccessor(h)方法。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 更新为0,进行复位
compareAndSetWaitStatus(node, ws, 0);
// 注意:获取头节点的下一个节点进行唤醒的,因为头节点是持有锁的节点。
Node s = node.next;
// s.waitStatus表示已经被取消了,会循环从后到前,找到第一个等待中的线程节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 进行唤醒(此处,是唤醒了线程2的)
LockSupport.unpark(s.thread);
}
好了,至此结束aqs的循序渐进介绍,从概念,特性,结构,应用,最后通过对ReentrantLock分析,相信已对AQS的同步机制有了更好的理解。AQS对同步状态和队列进行了定义和抽象,JDK本身基于此提供了如ReentrantLock、CountDownLatch、Semaphore等一系列的具体实现,让我们普通人可以更加方便的运用到日常的开发当中,当然框架dubbo和netty已经大量使用,比如ReentrantLock。
参考:
AbstractQueuedSynchronizer框架 https://t.hao0.me/java/2016/04/01/aqs.html
Java多线程 20 - AbstractQueuedSynchronizer详解(1) https://blog.coderap.com/article/228
Java多线程与并发基础 https://www.bbsmax.com/R/QW5YjPVGdm/