1、概述
AbstractQueuedSynchronizer(AQS):抽象队列同步器,定义了一套多线程访问共享资源的同步器框架(同步:线程之间的通信、协作),许多同步类实现都依赖于它,如:Lock 包中的各种锁(ReentrantLock
、ReadWriteLock
)、concurrent
包中的各种同步器(如 CountDownLatch
、 Semaphore
、CyclicBarrier
),还有阻塞队列和线程池的实现都有使用到 AQS
2、锁原理 —— 信号量 vs 管程
在并发编程领域,有两大核心问题:
- 互斥:同一时刻只允许一个线程访问共享资源
- 同步:多线程之间如何通信、协作(有序执行)
一般这两大问题可以通过信号量和管程来解决
2.1、信号量
信号量(Semaphore):操作系统提供的一种进程间常见的通信方式,主要用来协调并发程序对共享资源的访问, 操作系统可以保证对信号量操作的原子性
实现原理:
- 信号量由一个共享整型变量 S 和两个原子操作 PV 组成,S 只能通过 P 和 V 操作来改变
- P 操作:即请求资源,意味着 S 要减 1,如果 S < 0, 则表示没有资源了,此时线程要进入等待队列(同步队列)等待
- V 操作: 即释放资源,意味着 S 要加 1, 如果 S 小于等于 0,说明等待队列里有线程,此时就需要唤醒线程
如图:
信号量机制的引入解决了进程同步和互斥问题,但信号量的大量同步操作分散在各个进程中不便于管理,还有可能导致系统死锁;另外,条件越多,需要的信号量就越多,需要更加谨慎地处理信号量之间的处理顺序,否则很容易造成死锁现象
基于信号量给编程带来的隐患,于是有了提出了对开发者更加友好的并发编程模型——管程
2.2、管程
管程:把所有进程对某一种临界资源的同步操作都集中起来,构成一个所谓的秘书进程。凡要访问该临界资源的进程,都需先报告秘书,由秘书来实现诸进程对同一临界资源的互斥使用,这种机制就是管程
管程是一种在信号量机制上进行改进的并发编程模型,解决了信号量在临界区的 PV 操作上配对的麻烦,把配对的 PV 操作集中在一起而形成的并发编程方法理论,极大降低了使用和理解成本
管程由四部分组成:
- 管程内部的共享变量
- 管程内部的条件变量
- 管程内部并行执行的进程
- 对于局部与管程内部的共享数据设置初始值的语句
管程就是一个对象监视器:任何线程想要访问该资源(共享变量),就要排队进入监控范围。进入之后,接受检查,不符合条件,则要继续等待,直到被通知,然后继续进入监视器。
信号量和管程两者是等价的,信号量可以实现管程,管程也可以实现信号量,只是两者的表现形式不同而已,管程对开发者更加友好
两者区别如下:
管程为了解决信号量在临界区的 PV 操作上的配对的麻烦,把配对的 PV 操作集中在一起,并且加入了条件变量的概念,使得在多条件下线程间的同步实现变得更加简单
那么管程是如何解决互斥和同步的呢?
互斥:任何线程想要访问临界区,必须首先获取共享资源,一次只能被一个线程拥有。在共享资源占有的情况下,如果再有其它线程想占有共享资源,就需要到同步队列中等候,等到获取共享资源的线程释放资源后,等待队列中的线程就可以去竞争共享资源了。这样就解决了互斥问题。
本质上管程是通过将共享资源及其对共享资源的操作(线程安全地获取和释放)封装起来来保证互斥性的
同步:通过条件变量及条件等待队列实现的。分两种情况:
- 某一个线程获取到共享资源后,不需要做其它额外操作,就会释放共享资源(解锁)去通知(notify,notifyAll)同步队列的下一个线程去获取共享资源
- 某一个线程获取到共享资源后,需要做额外操作,那么此线程就会去条件等待队列中等待,同时释放共享资源,通知同步队列的下一个线程去获取共享资源。获得许可的线程执行完临界区的逻辑后会唤醒条件等待队列中的线程,将它移到同步队列中 ,等到其获取共享变量再进行处理
在 Java 里,锁大多是依赖于管程来实现的,以 synchronized
为例,它的实现原理如下:
当多个线程同时访问一段同步代码时,只会有某一个线程获取到对象锁,即 Monitor 对象,并将 Monitor 的 _owner
变量设置为当前线程,Monitor 中的计数器 _count
加 1;其它的线程会进入 _EntryList
队列中。
若持有 monitor 的线程调用 wait()
方法,将释放当前持有的 monitor,_owner
变量恢复为 null,_count
自减 1,同时该线程进入 _WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放monitor ( 锁)并复位变量的值,以便其他线程进入获取 monitor (锁)
3、AQS 实现原理
3.1、概述
实现原理:在多线程访问共享资源时,若标识的共享资源【state
】空闲,则将当前获取到共享资源的线程设置为有效工作线程,共享资源设置为锁定状态(独占模式下),其他线程没有获取到资源的线程进入阻塞队列【CLH
】,等待当前线程释放资源后,继续尝试获取
如图:
3.2、AQS 结构
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
// 共享变量 state:volatile 保证线程可见性
private volatile int state;
// 双向链表的首结点
private transient volatile Node head;
// 双向链表的尾结点
private transient volatile Node tail;
// 父类属性:【独占模式】获取锁的当前线程
private transient Thread exclusiveOwnerThread;
// 节点定义【如下】
static final class Node {
//...
}
}
3.1、state —— 共享变量
通过 state
的值来判断加锁状态:
state = 0
时:无锁状态state > 0
时:已经有线程获得了锁。可能会发生锁的重入,每次重入会给state + 1
。例如:同一个线程多次获得同步锁 5 次,那么 state = 5。而在释放锁的时候,同样需要释放 5 次直到 state = 0 时,其它线程才有资格获得锁
节点分:
- Exclusive【独占模式】:
state
上限只有1;如:ReentrantLock
- Share【共享模式】:
state
上限大于1;如:Semaphore
、CountDownLatch
、ReadWriteLock
,CyclicBarrier
state
的访问方式有三种:get()
、set()
、compareAndSetState()
,且都是被 final 修饰。其中:
// 通过 CAS 操作来修改 state 状态,表示争抢锁的操作
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
通过 CAS 乐观锁的方式来做比较并替换:如果当前内存中的state的值和预期值 expect 相等,则替换为 update,更新成功返回 true,否则返回 false
3.2、同步等待队列
同步等待队列:AQS 中的队列,该队列【CLH 队列】的实现是一个双向链表,它表示所有等待锁的线程的集合。每个节点 Node 保存了当前线程的同步状态,等待状态,前驱和后继节点等
Node 结构如下:
static final class Node {
// 节点的模式:SHARED【共享模式】、EXCLUSIVE【独占模式】
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
/// 线程的等待状态
// 线程的等待状态 表示线程已经被取消
static final int CANCELLED = 1;
// 线程的等待状态 表示后继线程需要被唤醒
static final int SIGNAL = -1;
// 线程的等待状态 表示线程在 Condtion 上
static final int CONDITION = -2;
// 表示下一个 acquireShared 需要无条件的传播
static final int PROPAGATE = -3;
// waitStatus 的初始值时 0,使用 CAS 来修改节点的状态
volatile int waitStatus;
// 当前节点的前驱节点,当前线程依赖它来检查waitStatus,在入队的时候才
volatile Node prev;
// 当前节点的后继节点
volatile Node next;
// 【SHARED】模式:单向链表的后继
Node nextWaiter;
// 当前节点的线程
volatile Thread thread;
//如果节点处于共享模式下等待直接返回true
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回当前节点的前驱节点,如果为空,直接抛出空指针异常
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null) {
throw new NullPointerException();
}
else {
return p;
}
}
// 用来建立初始化的head 或 SHARED的标记
Node() {}
// 指定线程和模式的构造方法
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// 指定线程和节点状态的构造方法
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
节点分为两种模式:
- 独占式:其他线程只有在占有锁的线程释放后才能竞争锁,有且只有一个线程能竞争成功(同一时刻,仅有一个线程持有同步状态)
- 共享式:共享资源可以被多个线程同时占有,直到共享资源被占用完毕(同一时刻,多个线程同时执行)
AQS 是怎么使用这个队列的呢,既然是双向链表,操纵它自然只需要一个头结点和一个尾节点:
// 头结点,不代表任何线程,是一个哑结点
private transient volatile Node head;
// 尾节点,每一个请求锁的线程会加到队尾
private transient volatile Node tail;
那么这个同步队列大整体就应该是这样的: