AQS
AQS介绍
AbstractOwnableSynchronizer抽象类简称AQS是 Java 并发包Locks的核心基础框架,几乎所有的同步工具(如 ReentrantLock重入锁、Semaphore信号量、CountDownLatch倒计时锁存器)都基于 AQS 实现。其核心思想是通过 Sync Queue/CLH 队列(一种虚拟的双向队列)管理线程的阻塞与唤醒,并通过 volatile变量 维护同步状态(state)(如锁的持有次数)
继承AbstractOwnableSynchronizer(AOS)类并实现序列化
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
AOS 也是一个抽象类,主要两个方法 设置和获取独占线程
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
private transient Thread exclusiveOwnerThread;
//设置当前拥有独占访问权限的线程
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
//返回最后设置的独占线程,没有返回null
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
除了AQS还有一个抽象类继承AOS,那就是AQLS :AbstractOwnableSynchronizer抽象类
AQS和AQLS区别在于多个个L,代表的是long,两者最大的区别在于
AQS定义state字段类型int:private volatile int state;
AQSL定义state字段类型long: private volatile long state;
主要是在同步状态State定义范围,一个是4个字节 32位,一个是8个字节64位。业务场景中,资源数量有可能超过int范围,使用AQLS,但实际能超过int的数值的业务场景不多,主要还是AQS的使用
AQS核心结构解读
1. 同步状态(state)
volatile int 类型,表示锁的状态,一个计数器
// 关键字段volatile修饰
private volatile int state;
// 获取资源状态 几个方法都是final修饰 说明不能被重写
final getState();
// 设置资源状态
final setState();
// 通过 CAS 修改状态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.节点(Node)
先看代码里官方文档定义,定义了节点Node的头和尾,并且头和尾也是Node类型
//等待队列的head,延迟初始化。除了初始化,它只能通过方法setHead进行修改。注意:如果head存在,则其waitStatus保证不会被取消。
private transient volatile Node head;
//等待队列的尾部,延迟初始化。仅通过方法enq修改以添加新的等待节点。
private transient volatile Node tail;
这里从定义只能看出 一个有头节点Node head和尾节点Node tail的队列,再来看Node节点的定义
// 这是一个静态内部类 并且使用了final修饰该类说明不能被继承,即AQS类加载适合 Node类也加载初始化
static final class Node {
// 节点分为两种模式: 共享式和独占式
//共享式:允许多个线程同时访问同一个共享资源,代表:emaphore、CountDownLatch等
static final Node SHARED = new Node();
//独占式:资源是独占的,一次只能有一个线程获取 代表:synchronized关键字和ReentrantLock类
static final Node EXCLUSIVE = null;
//线程已取消等待
static final int CANCELLED = 1;
//后继节点需要唤醒
static final int SIGNAL = -1;
//节点在条件队列中等待
static final int CONDITION = -2;
//下一个acquireShared应无条件传播;意思就是 共享模式下需要传播唤醒信号
static final int PROPAGATE = -3;
// 节点状态值,官方文档还声明 默认为0 初始状态
volatile int waitStatus;
//前驱节点的指向:节点prev 链接指向==>>前置节点
volatile Node prev;
//后继节点的指向:节点next 链接指向==>>后续节点
volatile Node next;
//获取同步状态的线程
volatile Thread thread;
//链接到下一个节点等待条件,或特殊值SHARED
Node nextWaiter;
//如果节点正在共享模式下等待,则返回true
final boolean isShared();
//返回上一个节点,如果为null,则抛出NullPointerExceptio
final Node predecessor() throws NullPointerException {};
//以下几个构造方法 用于建立初始头部或共享标记
Node() {
}
//由addWaiter方法使用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//由条件Condition使用
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
根据Node类中定义的属性或方法中官方文档说明可以看出
1.有线程的引用Thread thread;一个Node节点表示一个线程,通过一个Node节点封装/引用了一个线程,可以用 thread=null 断开线程和节点两者关系
2.节点里面还定义了节点,如:前驱节点(Node prev)指向前一个节点、后继节点(Node next)指向后一个节点,说明节点Node通过一个个指向构成了从而构成了一个双向队列, 该队列官方文档定义为CLH队列
3.里面定义了一个Node节点的属性 waitStatus,nextWaiter 单从这里定义看不出什么用,继续往下找
3.CLH 队列对节点的管理
上面我们总结出了CLH队列(Craig, Landin, and Hagersten)是一个双向队列,我们再来看具体对队列的管理方式是怎么实现的,包括新节点加入队列和从队列取出节点
入列操作
首先如何触发入列操作的,上面我们看出AQS的CLH队列是一个个封装引用了线程的节点组成的,本质的作用就是对应多个线程的管理工作,触发入列操作的行为就应该是该线程想要获取锁但是没有获取到,那么进入队列被管理
在Lock锁框架中,获取锁的方法是lock(),已ReentrantLock类为例说明,简化代码示例
//步骤1 构建ReentrantLock 类调用lock方法
//ReentrantLock 有两个构造方法,默认使用的是 非公平锁方式,这里无参构造的是非公平锁
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 入口方法
//步骤2执行
// ReentrantLock.NonfairSync
public void lock() {
sync.lock(); // 委托给 Sync 的非公平实现
}
//步骤3 非公平锁实现(NonfairSync)
final void lock() {
if (compareAndSetState(0, 1)) // 直接尝试 CAS 获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入 AQS 的获取逻辑
}
//步骤4 AQS 的acquire方法 在这里使用tryAcquire方法快速获取锁如果获取到了就不会入队了
// 如果获取不到则 创建一个带有唤醒标识状态的节点入队
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁(子类实现)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 创建节点并入队
selfInterrupt(); // 恢复线程的中断状态
}
所以在获取锁失败情况下,创建一个带有唤醒标识的EXCLUSIVE节点入队,这一步注意是两个方法acquireQueued方法和addWaiter方法
先看加入队列的addWaiter方法,上面Node类中特别为该方法准备了对应的构造函数Node(Thread thread, Node mode){},this.nextWaiter = mode;
private Node addWaiter(Node mode) {
//创建新节点:关联当前线程并封装成新节点,并使用以给定的模式(独占/共享)
Node node = new Node(Thread.currentThread(), mode);
//快速尝试直接添加到队尾(非线程安全,但通过CAS 保证原子性)
// 获取当前尾节点并定义成pred,(尾节点 Node tail定义private transient volatile Node tail;)
Node pred = tail;
// 通过对队尾的空判断,判断队列不为空(也就是判断队列是否初始化完成)
if (pred != null) {
// 原队尾赋值为新节点的前驱属性:新节点的前驱属性 指向 原队尾
node.prev = pred;
// 尝试通过 CAS 将新节点设为新队尾
if (compareAndSetTail(pred, node)) {
// 原队尾的后继 指向 新节点(即新节点成了队尾节点)
pred.next = node;
// 返回新节点
return node;
}
}
// 如果快速加入失败,则通过enq方式入列
enq(node);
return node;
}
总结:先通过addWaiter(Node node)方法尝试快速将该节点加入到队列中,加入方式是:将新节点设置成尾节点,并设置pred,next新旧尾节点的相互指向,是从队尾加入队列的,并且是相互指向 所以是一直基于链表形式的双向队列
在代码里if (pred != null) 如果flase,说明队列为空不存在,就走 enq(final Node node) 方法
private Node enq(final Node node) {
for (;;) { // 死循环就是自旋,说明是一直自旋判断直到成功入队
// 获取当前尾节点
Node t = tail;
// 通过获取到的尾节点判空方式,判断队列是否未初始化(尾节点为空 说明队列没东西)
if (t == null) {
// 创建虚拟头节点(占位节点,无关联线程),通过CAS操作设置
if (compareAndSetHead(new Node())) {
// 头尾指向同一虚拟节点,队列就初始化了,因为是死循环,下次就走不到这里了
tail = head;
}
} else {
//走到这里说明队列存在,并将原队尾赋值为新节点的前驱属性:新节点的前驱属性 指向 原队尾
node.prev = t;
// 尝试通过 CAS操作方法 将新节点设为新队尾,更新队尾节点
if (compareAndSetTail(t, node)) {
//将原队尾的后继 指向 新节点(即新节点已经成了队尾节点)到这里新节点加入队列就成了
t.next = node;
//已经自旋加入成功,那就return退出死循环,注意:返回值是原队尾,不是新节点
return t; //疑问:为啥返回原队尾不返回新节点,后面解释
}
}
}
}
总结:走到enq(final Node node)说明队列为空,自旋(死循环)方式不断尝试添加新节点直到成功
步骤:
1.第一次循序,先初始化CLH队列,方式是虚拟一个节点并同时定义为头 尾节点完成初始化,当前队列只存在一个虚拟节点,并且同时是头节点,尾节点
2.第二次循环,发现CLH队列不为空了(虽然里面只有一个虚拟节点),这个时候添加新节点称为队尾节点并返回原尾节点作为返回值,结束自旋,这个时候CLH队列节点有了两个:虚拟头节点.新队尾节点
网上看了很多CLH入列示意图,但可能和我理解的不一致,我基于上述代码解读做简单示意图
分两种情况
CLH未完成初始化,为空还不存在,新节点加入,enq方法代码解读
CLH已完成初始化,新节点加入,addWaiter方法实现的入列方式
总结:
1.新节点加入队列之前先判断是否存在队列,如果没有则初始化CLH队列再加入,自旋直到成功
2.从队尾加入,并更新成新的队尾,同时更新prev 和next指向
3.节点前后都有指向,说明是一个双向链接式队列,同时并没有明确定义出CLH,只是通过prev和next 一个个相互指向将使用节点关联起来的,所以这是个虚拟队列,加入队列方式存在自旋,所以也是一个自旋锁队列
入列后的自旋检查(acquireQueued)
这个方法单独拿出来讲,是因为觉得这个方法对应CLH队列的出入列管理中间桥梁,很重要
addWaiter执行完毕入队成功之后,但是注意在上面讲的获取锁步骤4里面,addWaiter方法将原来的Node节点 关联了线程新封装成一个节点并入队,然后,将新节点作为参数给acquireQueued
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
那么我们再来看看acquireQueued方法又做了什么
// 首先看到这是一个布尔类型的方法,并且带有异常处理
final boolean acquireQueued(Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;//for循环外中断标识
for (;;) { // 自旋循环
// 获取前驱节点
Node p = node.predecessor();
//自旋循环步骤1 前驱是头节点且获取锁成功
if (p == head && tryAcquire(arg)) {
setHead(node); // 成为新头节点
p.next = null; // 断开旧头节点 help GC
failed = false; //当前节点成为新节点退出循环前更新finally代码块执行标识
return interrupted;
}
//自旋循环步骤2 检查是否需要阻塞,若需要则调用 park()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 阻塞线程
interrupted = true;
}
}
finally {
if (failed)
// 异常时取消节点
cancelAcquire(node);
}
}
带有布尔类型返回的方法,使用死循环方式,跳出死循环方式从代码看就一种方式:循环判断1成功
循环判断条件1:if (p == head && tryAcquire(arg))
这里对象p是 node.predecessor(); 也就是当前节点的前驱节点,p == head判断前驱节点是否为头节点,而tryAcquire(arg)是判断当前节点封装引用线程是否已经尝试获取到锁(arg参数还是从上一步的acquire(int arg)拿到的),如果执行成功就设置当前节点成为头节点
方式如下
setHead(node); // 成为新头节点
p.next = null; // 断开旧头节点 help GC
//setHead(node)方法:将当前节点设为头节点,并断开与前驱节点的关联
private void setHead(Node node) {
head = node;
node.thread = null; // 置空thread字段
node.prev = null; // 断开prev指针
}
能执行到这里说明tryAcquire(arg)是ture,说明当前线程已经尝试并获取到锁了,所以成为头节点里面才有置空thread字段操作,断开与当前引用线程的关联(因为线程已经拿到锁开始执行任务去了,不需要在被管理了),好处是线程减少不必要的引用,减少内存开销同时防止线程内存泄露。而prev的置空切断与之前的前驱节点关联, p.next = null这一步又将原来的前驱节点和即将成为新头节点的当前节点后续节点关联切断,示意图如下
节点成为新的节点就会置空prev和thread(也是我们前面提到的),就剩个壳了,所以头节点相当于占位符性质,这里也验证了CLH队列遵循FIFO原则,先进先出,从队尾入列,从队首出列,当前节点设置成为新头节点也就返回ture结束循环了
这里额外插一句,我们在enq方法返回值t是当前节点的前驱节点,addWaiter方法最后又返回的当前节点,所以acquireQueued方法接受的是当前节点,但是为什么enq方法不返回当前节点而是要返回前驱节点,是因为enq方法被多次引用,在acquireQueued方法体现是:如果是走到了enq方法让节点加入队列的,那么最终返回的当前节点Node对象还带有前驱节点的关联指向引用,从而获取前驱节点可以被直接引用,不用再遍历查找前驱节点,减少CPU使用提升性能
循环判断2:检查是否需要阻塞,若需要则调用 park()
走到这一步意味着当前节点没能成为头节点,原因两点
①可能是前驱节点还不是头节点
②前驱节点是头节点了,但是当前节点引用的线程竞争获取锁没能成功
循环判断2 使用了两个判断方法对上面可能的原因作处理,并根据判断结果决定是否更新interrupted 值,注意这里没有返回值的,也就是无论成功与否都会跳到外层循序
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
shouldParkAfterFailedAcquire(p, node)
确保前驱节点的 waitStatus 为 SIGNAL(SIGNAL:表示前驱释放锁时会通知当前节点)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱解读的状态
int ws = pred.waitStatus;
//步骤1 前驱已标记为SIGNAL,当前线程可安全挂起(阻塞)等待
if (ws == Node.SIGNAL)
return true;
//步骤2 前驱状态为CANCELLED,CANCELLED状态表示取消等待的解读,需要被清理掉
if (ws > 0) {
do {
node.prev = pred = pred.prev;
}
// 跳过状态为CANCELLED的节点,重建链表
while (pred.waitStatus > 0);
pred.next = node;
} else {
// 状态为0或PROPAGATE,将前驱的waitStatus设为 SIGNAL(需 CAS 操作)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//再次尝试获取锁
return false;
}
步骤1.检查到前驱节点状态为SIGNAL,直接返回 true,线程可安全挂起 (还没排到我,可以老实等着)
步骤2.检查到前驱节点状态为CANCELLED,向前遍历跳过所有取消的节点,重新链接到有效前驱,返回 false 以重试获取锁
步骤3.检查到前驱节点状态为其他状态(0 或 PROPAGATE),通过 CAS 将前驱状态设为 SIGNAL,返回 false 以在下一次循环中检查
如果该方法返回false,意味着当前节点有可能是头节点的后续节点,从而通过返回flase直接退出判断2,再次进入外层循环(并且没有执行方法parkAndCheckInterrupt),然后再次进入判断条件1内 重新尝试获取锁,这种重试机制,既避免无意义的挂起,又确保队列的及时清理
parkAndCheckInterrupt()
方法走到这一步意味着shouldParkAfterFailedAcquire返回的是true(两个条件关联的是 并且关系),那么就认为当前线程就应该被挂起阻塞等待,排队服从队列管理
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞线程(执行在此暂停)
return Thread.interrupted(); // 唤醒后检查中断状态
}
这里使用了俩个方法LockSupport.park(this)和返回值Thread.interrupted()
park方法是挂起当前线程,unpark方法进行解除挂起,该方法是操作系统级别执行的
public static void main(String[] args) {
System.out.println("main start");
Thread t1 = new Thread(() -> {
System.out.println("t1 start");
LockSupport.park();
System.out.println("t1 end");
});
t1.start();
LockSupport.unpark(t1);
System.out.println("main end");
}
打印台
main start
main end
t1 start
t1 end
返回 return Thread.interrupted() 返回线程中断标识状态并清除线程中断标识
①如果return Thread.interrupted() 返回的是ture,那就说明判断条件成立,并且线程中断过(即 interrupted = true)并且现在被清除了中断标识,更新显示定义的interrupted = true;
②如果return Thread.interrupted() 返回的是false,说明线程未被中断过,不用处理interrupted =false
退出判断 继续执行外层循环
这样做的好处是:
①通过不断循环 如果有中断不断清除中断标识这种方式,延迟处理中断,确保锁获取成功后再响应
属于不可中断锁获取,让线程不断尝试获取锁过程不因为中断浪费获取到锁的机会
② 而显示定义的interrupted =false/true 属于中断标识记录事件方式 ,通过这个结果告诉调用者这个线程曾经发生过中断或者没有,记录中断事件但暂不处理
acquireQueued 方法最终退出循环方式两种
1.获取到锁并成为头节点
2.异常发生了被迫终止
而返回值就是boolean interrupted 显示定义的中断标识记录,那最终返回到上一层代码里
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如果 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))为ture 就是返回的boolean interrupted 为ture,代表线程曾经中断过并且在parkAndCheckInterrupt方法中被清除了中断标识,那我们就需要恢复线程的中断标识,方法selfInterrupt();防止因 Thread.interrupted() 清除标志导致中断丢失,同时将中断标识管理的操作给到上一层代码去处理
这种设计既保证了锁获取的可靠性,又维护了中断机制的灵活性,都是 AQS 框架高效并发控制的核心细节
出列操作
出列操作比较有意思,其最底层实现出列操作的方法很简单,但触发出列操作行为方式比较麻烦
出列的行为就像我们在上面acquireQueued方法讲的
setHead(node); // 成为新头节点
p.next = null; // 断开旧头节点 help GC
将当前节点设置为头节点后,那前面的节点就会变成没有指向孤立无缘的节点,会陆续配GC回收掉,者就是出列
就像入列触发是Lock方法,出列操作的触发方法入口就是unLock方法,Lock方法和unLock方法是对立的,lock方法:当前线程尝试获取锁,unLock方法:当前线程尝试释放锁
1.已ReentrantLock.unlock() 入口为例介绍
// ReentrantLock.java
public void unlock() {
sync.release(1); // 调用AQS的release方法
}
2.AQS 的 release 方法
// AbstractQueuedSynchronizer.java
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁(由子类实现)
Node h = head; // 获取当前头节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
3.这里判断条件是tryRelease 方法,方法实现的是锁的的释放逻辑,返回值是锁是否完全被释放
// ReentrantLock.Sync.java
protected final boolean tryRelease(int releases) {
// 计算释放后的状态
int c = getState() - releases; // 例如,若锁被重入 3 次(state=3),每次释放 releases=1,需多次调用 unlock() 直至 c=0
// 检查当前线程是否是锁的持有者
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 锁完全释放(无重入)
if (c == 0) {
free = true;
setExclusiveOwnerThread(null); // 清空持有线程
}
setState(c); // 更新锁状态
return free; // 返回是否完全释放
}
从这个方法中可以看出,子类实现 tryRelease 时需满足以下条件:
①.线程安全性:必须确保只有持有资源的线程可以释放它。
②.状态管理:正确更新同步状态(如 state 字段)。
③.重入支持:处理可重入场景(如锁的多次获取和释放)
4.tryRelease 返回ture,代表锁已经完全被释放了,就可以进入到unparkSuccessor方法: 唤醒后继节点(提醒后面的节点可以参与到锁竞争了)
// AbstractQueuedSynchronizer.java
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 清空头节点状态
Node s = node.next; // 获取后继节点
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)
LockSupport.unpark(s.thread); // 在入队之后做的park处理的线程,直接跳转到那里
}
LockSupport.unpark(s.thread)这里是通过特殊的机制方法park unpark方法,直接跳转到上面将的parkAndCheckInterrupt方法的park方法去,然后在acquireQueued 方法循序执行:尝试获取锁,然后成为头节点 这两件事
5.被唤醒线程的后续流程(acquireQueued 循环),当后继节点线程被唤醒后,继续执行 acquireQueued 中的循环:这个方法代码上面有了
以上就是尝试释放锁并成功释放锁触发的出列操作流程,但是在尝试释放锁过程中存在可能发生的异常,比如尝试释放锁超时,发生中断了,或者调用者主动阻塞取消线程了,那么在整个方法体里的异常处理兜底机制就发生作用了,最终兜底方法是cancelAcquire();这个方法处理逻辑代码如下
// AbstractQueuedSynchronizer.java
private void cancelAcquire(Node node) {
if (node == null) return;
// 清空关联的线程(避免内存泄漏)
node.thread = null;
// 跳过已取消的前驱节点(CANCELLED状态)
Node pred = node.prev;
while (pred != null && pred.waitStatus > 0) {
node.prev = pred = pred.prev;
}
Node predNext = pred.next;
// 标记当前节点为取消状态
node.waitStatus = Node.CANCELLED;
// 根据不同场景修复队列链接
if (node == tail && compareAndSetTail(node, pred)) {
// 场景1:当前节点是尾节点
compareAndSetNext(pred, predNext, null);
} else {
// 场景2:当前节点是中间节点或头节点附近
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 将前驱的next指向当前节点的后继(跳过当前节点)
Node next = node.next;
if (next != null && next.waitStatus <= 0) {
compareAndSetNext(pred, predNext, next);
}
} else {
// 兜底逻辑:唤醒后继节点(防止队列断裂)
unparkSuccessor(node);
}
// 自环指针,辅助GC识别不可达节点
node.next = node;
}
}
步骤 1:清空线程引用
步骤 2:跳过已取消的前驱节点
步骤 3:标记节点为取消状态
步骤 4:修复队列有效链接
最终在兜底方法unparkSuccessor(node);又回到了LockSupport.unpark(s.thread);这里然后和上面一样在到acquireQueued 方法里执行
AQS 内部类之ConditionObject
ConditionObject的介绍链接,该内部类示例的产生是在Lock接口的方法new Condition,所以所有实现Lock接口的类理论上都可以使用到它,工作机制:通过条件队列和同步队列的交接管理,实现线程的精准唤醒
AQS 的两种模式区别的本质
上面在Node节点类解读看到有两种模式 独占和占用
独占模式(Exclusive)
特点:同一时刻只有一个线程能获取资源如 ReentrantLock、WirteLock
共享模式(Shared)
特点:多个线程可同时获取资源如 Semaphore、CountDownLatch、ReadLock
我们通过State的在不同模式下的定义认识下这两种模式,state 是AQS定义的字段:一个 int的变量,它本身没有固定含义,子类通过不同的操作逻辑赋予其语义
独占模式:
- ReentrantLock:state 表示锁的状态(如 0 表示未锁定,>0 可重入,表示锁定次数)
- WriteLock:state的低16位置表示写锁状态(如 0 表示未锁定,>0 可重入,表示写锁获取次数)
共享模式:
- ReadLock:state的高16位表示读锁状态(如 0 表示未锁定,>0 可重入,表示读锁获取次数)
- Semaphore:state 表示可用资源数(如信号量的许可证数量),如初始化值定义state为3表示同时最多三个线程同时获取锁执行,其余的线程去入队等待
- CountDownLatch:倒计数锁,表示当前线程在能尝试获取锁执行前还剩余几个执行线程,如初始化值定义3,表示当前线程前面还有3个线程在执行,入队等待前面完事,计数值归0了才可以获取锁执行
参考synchronized实现锁管理机制的底层原理,AQS锁机制本质是:定义一个锁对象(类似监控对象Monitor),如ReentrantLock,通过队列管理决定哪个线程可以允许执行(出入队列类似监控进入进入进出),队列管理的方法就是通过计数值state(类似程序计数器的设定),在多线程下程序计数器state变更的用CAS操作,保证原子性
以下简单示例说明锁获取过程中state的作用
独占模式 获取锁方法
protected boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 当前 state
if (c == 0) { // 锁未被占用
if (compareAndSetState(0, acquires)) { // CAS 尝试获取
setExclusiveOwnerThread(current); // 设置独占线程
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 可重入逻辑
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc); // 直接更新 state(无需 CAS,因为已持有锁)
return true;
}
return false; // 获取失败
}
会通过c == 0判断锁是否被占用,如果没有被占用 更新state值从0->1,并设置独占线程,在这里的state的设定就是计数值+1的,如果当前线程尝试获取锁之前已经持有锁,还是累加1,允许线程多次获取锁,这就是重入锁的设定,反过来讲 那每次state如果有等于0 则表示没有线程使用锁。
为什么是独占模式:因为state的每次更新操作,都会结合判断当前线程 是否 是最新设置的独占线程,这种保证了每次操作执行同一时间只有一个线程
共享模式 获取锁方法
protected int tryAcquireShared(int acquires) {
for (;;) { // 自旋直到成功或失败
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || // 资源不足,直接返回负数(失败)
compareAndSetState(available, remaining)) { // CAS 尝试减少 state
return remaining; // 返回剩余资源数(可能为负数)
}
}
}
共享模式下直接获取全部剩余的状态值,并且和我们希望许可值比较看还是否资源足够,这里面并无线程所有权, 也没有给具体的线程设置锁状态,从而也验证了共享模式下多个线程可同时获取我们许可数量的资源
AQS的模板方法区分说明
模板方法顾名思义,提供多线程实现同步管理的模板处理准则,AQS的方法确实多,看着比较乱,但核心的是方法名里面带有(获取锁)Acquire和(释放锁)Release的方法,这类型方法归为模板方法,AQS里面剩余的方法的可以说都是为这些模板方法服务的,我们针对这些模板方法做下区分说明
区分方式一:方法的工作性质
在AQS里面核心模板方法有三种类型,try******,do****,还有不带try和do的方法入口方法**
,区别
1.try类型的方法 尝试快速获取锁,特点是带方法名带try,这种方法都是每个继承AQS的子类单独实现,特点2需要子类实现的方法,这是因为每个子类对应state值的管理定义不同,所以try类型的方法都是单独每个子类去实现的,这类型方法围绕着state操作处理,还有一个特点3立即给出结果,就是快速获取锁成不成都给你个结果,因为获取锁不成要入队的呀,示例:tryAcquire,tryRelease,tryAcquireShared,tryReleaseShared
2.do类型的方法,do可以解释成do work干活的,特点是方法名带do,这类型的方法是AQS模板方法里面 真正有具体工作步骤的,是模板方法里面真正干活的,干的事情围绕着 挂起线程 队列管理和 解挂唤醒线程,这类型方法特点之一是final修饰方法,明不可重写 只能引用,示例:doAcquireShared,doReleaseShared,doAcquireNanos,
至于特例是acquireQueued
,虽然它名字不带do 但也是真正干活的 它例外记住就行
3.入口类型的模板方法,例如acquire,acquireShared ,这些方法都是一些入口方法,他们的作用就是将上面两者类型的方法结合到一块,就是将子类实现的state管理 和 AQS工作的具体队列管理 解挂唤醒线程操作 结合起来,代码示例
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
区分模式二:模式不同的方法
这点好区分,方法带share的都是共享模式的方法,不带的就是独占模式的方法
区分模式三:方法处理线程挂起和唤醒机制的不同
AQS提供state的处理,决定是否入队,入队后做的是等待线程被唤醒,然后继续尝试获取锁执行任务,那么线程冲入队等待挂起是park,那么唤醒的分为三种
1.可以响应中断的,即中断挂起操作的,特点是方法名带Interruptibly,如doAcquireInterruptibly
2**.超时唤醒**的,这类型的可以响应中断,另外到时间也自动唤醒,方法名带Nanos 如doAcquireNanos
3.除去以上两种,剩下的那种就是入队老老实实排队等待轮到线程被唤醒,并且不可中断(不响应中断),处理方式是将中断标识抛给上一层代码去处理,自己只管排队等待被unPark