AQS详解
AQS 的全称为 AbstractQueuedSynchronizer
,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks
包下面
AQS 就是一个抽象类,主要用来构建锁和同步器。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}
AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock
,Semaphore
,其他的诸如 ReentrantReadWriteLock
,SynchronousQueue
等等皆是基于 AQS 的。
AQS结构图
- 上图中有颜色的为Method,无颜色的为Attribution
- AQS框架共分为5层,自上而下,由浅入深,由AQS对外暴露的API到底层基础数据。
- 当有自定义同步器接入时,只需要重写第一层所需要的部分即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或解锁操作时,先经过第一层的API进入AQS
AQS核心思想
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
主要原理图:
Node节点
要加入队列的线程会被封装为Node对象加入到队列中
解释一下几个方法和属性值的含义:
方法和属性值 | 含义 |
---|---|
waitStatus | 当前节点在队列中的状态 |
thread | 表示处于该节点的线程 |
prev | 前驱指针 |
predecessor | 返回前驱节点,没有的话抛出npe |
nextWaiter | 指向下一个处于CONDITION状态的节点(由于本篇文章不讲述Condition Queue队列,这个指针不多介绍) |
next | 后继指针 |
线程两种锁的模式:
模式 | 含义 |
---|---|
SHARED | 表示线程以共享的模式等待锁 |
EXCLUSIVE | 表示线程正在以独占的方式等待锁 |
waitStatus有下面几个枚举值:
枚举 | 含义 |
---|---|
0 | 当一个Node被初始化的时候的默认值 |
CANCELLED | 为1,表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化 |
CONDITION | 为-2,表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁 |
PROPAGATE | 为-3,当前线程处在SHARED情况下,该字段才会使用,SHARED模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点 |
SIGNAL | 为-1,表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL |
同步状态state
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过procted类型的getState,setState,compareAndSetState进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
这几个方法都是Final修饰的,说明子类中无法重写它们。我们可以通过修改state字段表示的同步状态来实现多线程的独占模式和共享模式
AQS重要方法
方法名 | 描述 |
---|---|
protected boolean isHeldExclusively() | 该线程是否正在独占资源。只有用到Condition才需要去实现它。 |
protected boolean tryAcquire(int arg) | 独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。 |
protected boolean tryRelease(int arg) | 独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。 |
protected int tryAcquireShared(int arg) | 共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 |
protected boolean tryReleaseShared(int arg) | 共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。 |
一般来说自定义的同步器要么是独占方式要么是共享方式,它们也只需要实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,比如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease。
接下来我们通过ReentrantLock的源码来讲解AQS的核心思想
根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
下面我们来看一下公平锁与非公平锁的加锁方法的源码:
通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。
再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。
当我们使用AQS自定义同步器的lock方法时会经历哪几个方法呢?
以ReentrantLock的非公平锁为例
Lock过程
lock方法
// reentrantLock中自定义的NonfairSync类
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
@ReservedStackAccess
final void lock() {
if (compareAndSetState(0, 1)) // 通过CAS尝试修改state获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 修改state失败,进入到AQS的acquire方法中
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
(AQS)acquire方法
@ReservedStackAccess
public final void acquire(int arg) {
//
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 去到reentrantLock实现的tryAcquire方法尝试获取锁
- 如果没有获取到锁,就触发AQS的acquireQueued方法
(reentrantLock)tryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
(reentrantLock)nonfairTryAcquire方法
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
- 通过getState()方法获取到state的值,如果state == 0,通过CAS修改state的值尝试获取锁
- 如果state != 0,说明当前资源被线程占有着,判断占有该资源的线程是否是当前线程,如果是说明是重入了,state += acquires
(AQS)addWaiter方法
// 将节点添加到链表的尾部
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // CAS,防止在添加节点的过程中有其他节点插入
pred.next = node;
return node;
}
}
// 如果tail节点为空,或者CAS失败,进入到end方法
enq(node);
return node;
}
(AQS)end方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果尾节点为空,表示队列为空,需要初始化
if (t == null) {
// 必须进行初始化,尝试设置头节点
if (compareAndSetHead(new Node()))
// 初始化成功后,将尾节点也设置为头节点
tail = head;
} else {
// 如果尾节点不为空,表示队列非空,直接将新节点加入队尾
node.prev = t; // 将新节点的前驱设置为当前尾节点
if (compareAndSetTail(t, node)) {
// 设置尾节点成功后,更新原尾节点的next引用为新节点
t.next = node;
return t; // 返回原尾节点
}
}
}
}
简而言之,end的作用就是在队列初始化,以及插入节点失败的时候触发,确保成功插入节点
(AQS) acquireQueued方法
@ReservedStackAccess
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果前驱是头节点且尝试获取锁成功
if (p == head && tryAcquire(arg)) {
setHead(node); // 将当前节点设置为头节点
p.next = null; // 帮助GC,断开前驱的next引用
failed = false; // 获取锁成功,不需要取消获取
return interrupted; // 返回是否被中断过
}
// 判断是否需要挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true; // 线程被中断
}
} finally {
if (failed)
cancelAcquire(node); // 如果获取锁失败,取消获取
}
}
acquireQueued就是在获取当前节点的前驱节点,判断前驱节点是否是头节点并且能够获取(资源),代表当前节点是否能够占有锁,设置头节点为当前节点,返回。否则,调用shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法
(AQS) shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
解释:
int ws = pred.waitStatus;
获取前驱节点的等待状态。- 如果前驱节点的等待状态为
Node.SIGNAL
,表示前驱节点已经设置了状态要求释放,可以安全地挂起当前线程。- 返回
true
,表示需要挂起。
- 返回
- 如果前驱节点的等待状态大于 0,表示前驱节点被取消(cancelled),需要跳过已被取消的前驱节点并指示重试。
- 使用循环遍历跳过已被取消的前驱节点,并设置新的前驱节点。
- 返回
false
,表示不需要挂起,需要重试。
- 如果前驱节点的等待状态为 0 或
Node.PROPAGATE
,表示需要一个信号,但此时不要挂起。调用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
尝试将前驱节点的等待状态设置为Node.SIGNAL
,表示需要一个信号。- 返回
false
,表示不需要挂起,需要重试。
- 返回
这个方法主要用于在获取锁失败后判断线程是否需要挂起,根据前驱节点的等待状态进行不同的处理。在多线程环境下,通过 compareAndSet
操作确保了对前驱节点等待状态的原子性更新。如果需要挂起,返回 true
,否则返回 false
。
(AQS)parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
parkAndCheckInterrupt主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。
上面方法的流程图:
从上图可以看出,跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,具体挂起流程用流程图表示如下(shouldParkAfterFailedAcquire流程):
从队列中释放节点的疑虑打消了,那么又有新问题了:
- shouldParkAfterFailedAcquire中取消节点是怎么生成的呢?什么时候会把一个节点的waitStatus设置为-1?
- 是在什么时间释放节点通知到被挂起的线程呢?
再看final块中的cancelAcquire方法
(AQS)cancelAcquire方法
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
// node为空,返回
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
// 保存node的前驱节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
// 获取pred节点的下一个节点
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
// 设置node节点的状态为CANCELLED
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) { // node节点为尾节点,设置尾节点为pred节点
// 更新成功,将tail的后继节点设置为null
compareAndSetNext(pred, predNext, null);
} else { // node节点不为尾节点,或者CAS设置不成功
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
// 如果当前节点不是head的后继节点,1.判断当前节点的前驱节点是否是SINGNAL 2.如果不是,则把前驱节点设置为SIGNAL看是否成功
// 1和2有一个为true,在判断当前节点的线程是否为null
// 如果上面的条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 如果当前节点是head的后继节点,或者上面的条件都不满足,那就唤醒当前节点的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
该方法完成的功能就是取消当前线程对资源的获取,即设置该节点的状态为CANCELLED
通过上面的流程,我们对于CANCELLED节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作?
执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了),如果此时修改Prev指针,有可能会导致Prev指向另一个已经移除队列的Node,因此这块变化Prev指针不安全。 shouldParkAfterFailedAcquire方法中,会执行下面的代码,其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更Prev指针比较安全。
do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0);
acquire总结
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* Convenience method to interrupt current thread.
* 中断当前线程
*/
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
- 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回
- 没成功,则addWaiter将该线程加入等待队列的尾部,并标记为独占模式
- acquireQueue()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才进行自我中断selfInterrupt(),将中断补上
如果tryAcquire返回true,表示成功获取到了资源,此时调用selfInterrupt()就会唤醒等待中的线程,如果线程本来就在运行,调用Thread.currentThread().interrupt();依然不会影响运行;如果tryAcquire返回false,acquireQueued返回true表示中断过,此时中断标志位为true,再调用selfInterrupt会把中断标志位设置为false,依然不会唤醒等待中的线程。
Release过程
同样以ReentrantLock为例
(ReentrantLock) unlock方法
public void unlock() {
sync.release(1);
}
从ReentrantLock的unlock开始追
(AQS)release方法
@ReservedStackAccess
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
由于ReentrantLock中的sync继承了AQS,所以追到了AQS的release方法,可以看到释放锁的逻辑主要还是由tryRelease决定,而tryRelease方法是ReentrantLock重写的方法,所以释放锁的逻辑主要是由自定义同步器决定的
(ReentrantLock) tryRelease方法
@ReservedStackAccess
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);
}
setState(c);
return free;
}
主要步骤:
- 获取state值
- 判断占有锁的线程和当前线程是否相等,不想等抛出异常
- 如果state - release == 0,说明可以释放锁,setExclusiveOwnerThread(null)将占有锁的线程对象置为null
- 更新state值
回到Release方法,为什么需要判断h != null && h.waitStatus != 0?
h == null 说明head可能还没被初始化。初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null 的情况。
h != null && waitStatus == 0 说明后继节点对应的线程已经在运行中,不需要唤醒
h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要使用unparkSuccessor(h)唤醒
(AQS) unparkSuccessor方法
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
解释:
int ws = node.waitStatus;
获取当前节点的等待状态。- 如果当前节点的等待状态小于 0(
ws < 0
),尝试使用compareAndSetWaitStatus(node, ws, 0)
将当前节点的等待状态清零。这是为了在发出信号之前尝试清除等待状态,以便提前准备好进行信号。 Node s = node.next;
获取当前节点的后继节点。- 如果后继节点为空或者后继节点的等待状态大于 0,表示后继节点被取消或者等待状态不符合要求,需要从尾节点向前遍历找到实际的非取消状态的后继节点。
- 使用循环遍历,寻找非取消状态的后继节点。
- 如果找到非取消状态的后继节点(
s != null
),使用LockSupport.unpark(s.thread)
唤醒后继节点的线程。
为什么要从后往前找到第一个Cancelled的节点?
之前的addWaiter方法:
// 将节点添加到链表的尾部
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { // CAS,防止在添加节点的过程中有其他节点插入
pred.next = node;
return node;
}
}
// 如果tail节点为空,或者CAS失败,进入到end方法
enq(node);
return node;
}
我们从这里可以看到,节点入队并不是原子操作,也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。
综上所述,如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点。
使用AQS自定义同步器
下面是一个简单的使用 AQS 自定义同步器的例子,实现一个简化的互斥锁(Mutex):
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
// 自定义同步器
class Mutex extends AbstractQueuedSynchronizer {
// 尝试获取锁
@Override
protected boolean tryAcquire(int arg) {
// 使用CAS原子操作尝试设置state值为1,表示获取锁成功
return compareAndSetState(0, 1);
}
// 尝试释放锁
@Override
protected boolean tryRelease(int arg) {
// 设置state值为0,表示释放锁成功
setState(0);
return true;
}
// 判断是否锁已被占用
@Override
protected boolean isHeldExclusively() {
// state为1时表示锁已被占用
return getState() == 1;
}
// 提供一个简单的外部获取锁的方法
public void lock() {
acquire(1);
}
// 提供一个简单的外部释放锁的方法
public void unlock() {
release(1);
}
}
public class CustomMutexExample {
public static void main(String[] args) {
// 创建自定义同步器的实例
Mutex mutex = new Mutex();
// 使用自定义同步器实现互斥锁
mutex.lock();
try {
// 执行需要同步的操作
System.out.println("Critical section - locked");
} finally {
// 释放锁
mutex.unlock();
System.out.println("Critical section - unlocked");
}
}
}
Mutex
类继承了 AbstractQueuedSynchronizer
并实现了其中的 tryAcquire
、tryRelease
和 isHeldExclusively
方法。这些方法定义了获取和释放锁的逻辑。通过调用 acquire
和 release
方法,可以在外部代码中使用这个简化的互斥锁
总结
Q1:当线程获取锁失败的后续流程是什么?
- 会将获取锁失败的线程存入到一个链表中,线程继续等待,仍然保留获取锁的可能
Q2:这个链表是一种什么结构呢?
- 是CLH变体的FIFO双端队列
Q3:链表中的等待线程是怎么获取到锁的呢?
- 如果是独占锁,在持有锁的线程释放了独占资源,即调用了release方法释放同步状态,那么等待队列中的第一个线程就有机会获取到锁。这是因为release方法通常会触发等待队列中的后继节点,使其尝试获取锁
Q4:如果处于排队等候机制的线程一直无法获取锁,需要一直等待吗?还是有别的策略来解决这一问题?
- 线程所在的节点状态会变成取消状态,取消状态的节点会从队列中释放
Q5:Lock函数通过Acquire方法进行加锁,但是具体是怎么加锁的呢?
- 首先Acquire方法最终会去触发到自定义同步器实现的tryAcquire方法,具体加锁的逻辑是有自定义同步器决定的。比如ReentrantLock就是通过CAS修改state值来进行加锁
Q6:AQS实现的锁和Synchronized的区别?
参考链接
- https://wardseptember.gitee.io/mynotes/#/docs/Java
- https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html