Java多线程与高并发专题——从AQS到ReentrantLock
关于AQS
AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。
其源码备注如下:
Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues. This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic {@code int} value to represent state. Subclasses must define the protected methods that change this state, and which define what that state means in terms of this object being acquired or released. Given these, the other methods in this class carry out all queuing and blocking mechanics. Subclasses can maintain other state fields, but only the atomically updated {@code int} value manipulated using methods {@link #getState}, {@link #setState} and {@link #compareAndSetState} is tracked with respect to synchronization.
<p>Subclasses should be defined as non-public internal helper classes that are used to implement the synchronization properties of their enclosing class. Class {@code AbstractQueuedSynchronizer} does not implement any synchronization interface. Instead it defines methods such as {@link #acquireInterruptibly} that can be invoked as appropriate by concrete locks and related synchronizers to implement their public methods.
<p>This class supports either or both a default <em>exclusive</em> mode and a <em>shared</em> mode. When acquired in exclusive mode, attempted acquires by other threads cannot succeed. Shared mode acquires by multiple threads may (but need not) succeed. This class does not "understand" these differences except in the mechanical sense that when a shared mode acquire succeeds, the next waiting thread (if one exists) must also determine whether it can acquire as well. Threads waiting in the different modes share the same FIFO queue. Usually, implementation subclasses support only one of these modes, but both can come into play for example in a {@link ReadWriteLock}. Subclasses that support only exclusive or only shared modes need not define the methods supporting the unused mode.
<p>This class defines a nested {@link ConditionObject} class that can be used as a {@link Condition} implementation by subclasses supporting exclusive mode for which method {@link #isHeldExclusively} reports whether synchronization is exclusively held with respect to the current thread, method {@link #release} invoked with the current {@link #getState} value fully releases this object, and {@link #acquire}, given this saved state value, eventually restores this object to its previous acquired state. No {@code AbstractQueuedSynchronizer} method otherwise creates such a condition, so if this constraint cannot be met, do not use it. The behavior of {@link ConditionObject} depends of course on the semantics of its synchronizer implementation.
<p>This class provides inspection, instrumentation, and monitoring methods for the internal queue, as well as similar methods for condition objects. These can be exported as desired into classes using an {@code AbstractQueuedSynchronizer} for their synchronization mechanics.
<p>Serialization of this class stores only the underlying atomic integer maintaining state, so deserialized objects have empty thread queues. Typical subclasses requiring serializability will define a {@code readObject} method that restores this to a known initial state upon deserialization.
<h3>Usage</h3>
<p>To use this class as the basis of a synchronizer, redefine the following methods, as applicable, by inspecting and/or modifying the synchronization state using {@link #getState}, {@link #setState} and/or {@link #compareAndSetState}:
<ul>
<li> {@link #tryAcquire}
<li> {@link #tryRelease}
<li> {@link #tryAcquireShared}
<li> {@link #tryReleaseShared}
<li> {@link #isHeldExclusively}
</ul>
Each of these methods by default throws {@link UnsupportedOperationException}. Implementations of these methods must be internally thread-safe, and should in general be short and not block. Defining these methods is the <em>only</em> supported means of using this class. All other methods are declared {@code final} because they cannot be independently varied.
<p>You may also find the inherited methods from {@link AbstractOwnableSynchronizer} useful to keep track of the thread owning an exclusive synchronizer. You are encouraged to use them -- this enables monitoring and diagnostic tools to assist users in determining which threads hold locks.
<p>Even though this class is based on an internal FIFO queue, it does not automatically enforce FIFO acquisition policies. The core of exclusive synchronization takes the form:
<pre>
Acquire:
while (!tryAcquire(arg)) {
<em>enqueue thread if it is not already queued</em>;
<em>possibly block current thread</em>;
}
Release:
if (tryRelease(arg))
<em>unblock the first queued thread</em>;
</pre>
(Shared mode is similar but may involve cascading signals.)
<p id="barging">Because checks in acquire are invoked before enqueuing, a newly acquiring thread may <em>barge</em> ahead of others that are blocked and queued. However, you can, if desired, define {@code tryAcquire} and/or {@code tryAcquireShared} to disable barging by internally invoking one or more of the inspection methods, thereby providing a <em>fair</em> FIFO acquisition order. In particular, most fair synchronizers can define {@code tryAcquire} to return {@code false} if {@link #hasQueuedPredecessors} (a method specifically designed to be used by fair synchronizers) returns {@code true}. Other variations are possible.
<p>Throughput and scalability are generally highest for the default barging (also known as <em>greedy</em>, <em>renouncement</em>, and <em>convoy-avoidance</em>) strategy. While this is not guaranteed to be fair or starvation-free, earlier queued threads are allowed to recontend before later queued threads, and each recontention has an unbiased chance to succeed against incoming threads. Also, while acquires do not "spin" in the usual sense, they may perform multiple invocations of {@code tryAcquire} interspersed with other computations before blocking. This gives most of the benefits of spins when exclusive synchronization is only briefly held, without most of the liabilities when it isn't. If so desired, you can augment this by preceding calls to acquire methods with "fast-path" checks, possibly prechecking {@link #hasContended} and/or {@link #hasQueuedThreads} to only do so if the synchronizer is likely not to be contended.
<p>This class provides an efficient and scalable basis for synchronization in part by specializing its range of use to synchronizers that can rely on {@code int} state, acquire, and release parameters, and an internal FIFO wait queue. When this does not suffice, you can build synchronizers from a lower level using {@link java.util.concurrent.atomic atomic} classes, your own custom {@link java.util.Queue} classes, and {@link LockSupport} blocking support.
<h3>Usage Examples</h3>
<p>Here is a non-reentrant mutual exclusion lock class that uses the value zero to represent the unlocked state, and one to represent the locked state. While a non-reentrant lock does not strictly require recording of the current owner thread, this class does so anyway to make usage easier to monitor. It also supports conditions and exposes one of the instrumentation methods:
<pre> {@code
class Mutex implements Lock, java.io.Serializable {
// Our internal helper class
private static class Sync extends AbstractQueuedSynchronizer {
// Reports whether in locked state
protected boolean isHeldExclusively() {
return getState() == 1;
}
// Acquires the lock if state is zero
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Releases the lock by setting state to zero
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Provides a Condition
Condition newCondition() { return new ConditionObject(); }
// Deserializes properly
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
// The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public boolean tryLock() { return sync.tryAcquire(1); }
public void unlock() { sync.release(1); }
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively(); }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}}</pre>
<p>Here is a latch class that is like a {@link java.util.concurrent.CountDownLatch CountDownLatch} except that it only requires a single {@code signal} to fire. Because a latch is non-exclusive, it uses the {@code shared} acquire and release methods.
<pre> {@code
class BooleanLatch {
private static class Sync extends AbstractQueuedSynchronizer {
boolean isSignalled() { return getState() != 0; }
protected int tryAcquireShared(int ignore) {
return isSignalled() ? 1 : -1;
}
protected boolean tryReleaseShared(int ignore) {
setState(1);
return true;
}
}
private final Sync sync = new Sync();
public boolean isSignalled() { return sync.isSignalled(); }
public void signal() { sync.releaseShared(1); }
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
}}</pre>
@since 1.5
@author Doug Lea
翻译:
提供了一个用于实现阻塞锁和相关同步器(如信号量、事件等)的框架,这些同步器依赖于先入先出(FIFO)等待队列。此类旨在成为大多数依赖单个原子 int 值来表示状态的同步器的有用基础。子类必须定义改变此状态的受保护方法,以及定义该状态在对象被获取或释放时的含义。基于这些定义,此类中的其他方法执行所有排队和阻塞机制。子类可以维护其他状态字段,但只有通过 getState、setState 和 compareAndSetState 方法操作的原子更新的 int 值在同步方面被跟踪。
子类应被定义为非公共的内部辅助类,用于实现其包含类的同步属性。AbstractQueuedSynchronizer 类本身并未实现任何同步接口。相反,它定义了一些方法,如 acquireInterruptibly,这些方法可以被具体的锁和相关同步器在实现其公共方法时适当调用。
此类既支持默认的独占模式,也支持共享模式。以独占模式获取时,其他线程的获取尝试无法成功。多个线程以共享模式获取时,可能会(但不是必须)成功。此类并不“理解”这些差异,只是在机械意义上,当共享模式获取成功时,下一个等待的线程(如果存在)也必须确定它是否可以获取。不同模式下等待的线程共享同一个 FIFO 队列。通常,实现子类只支持其中一种模式,但在某些情况下,两种模式都可能起作用,例如在 ReadWriteLock 中。仅支持独占模式或共享模式的子类无需定义未使用模式的方法。
此类定义了一个嵌套的 ConditionObject 类,可用作支持独占模式的子类的 Condition 实现,其中 isHeldExclusively 方法报告当前线程是否独占持有同步,release 方法使用当前 getState 值完全释放此对象,而 acquire 方法在给定保存的状态值后,最终恢复此对象到之前获取的状态。没有其他 AbstractQueuedSynchronizer 方法会创建这样的条件,因此如果无法满足此约束,请勿使用它。ConditionObject 的行为当然取决于其同步器实现的语义。
此类提供了用于检查内部队列、条件对象的工具方法,以及类似的监控方法。这些方法可以根据需要导出到使用 AbstractQueuedSynchronizer 进行同步机制的类中。
此类的序列化仅存储维护状态的底层原子整数,因此反序列化后的对象具有空的线程队列。需要序列化的典型子类将定义一个 readObject 方法,在反序列化时将此状态恢复到已知的初始状态。
要使用此类作为同步器的基础,请根据需要重新定义以下方法,通过检查和/或修改同步状态来实现:
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
这些方法默认都会抛出 UnsupportedOperationException。这些方法的实现必须是内部线程安全的,并且通常应该简短且不阻塞。定义这些方法是使用此类的唯一支持方式。所有其他方法都被声明为 final,因为它们不能独立变化。
您可能还会发现从 AbstractOwnableSynchronizer 继承的方法对于跟踪拥有独占同步器的线程很有用。建议使用这些方法——这有助于监控和诊断工具确定哪些线程持有锁。
尽管此类基于内部 FIFO 队列,但它不会自动强制 FIFO 获取策略。独占同步的核心形式如下:
Acquire:while (!tryAcquire(arg)) {<em>如果尚未排队,则将线程入队</em>;<em>可能阻塞当前线程</em>;} Release:if (tryRelease(arg))<em>解阻塞第一个排队的线程</em>;
共享模式类似,但可能涉及级联信号。
因为获取检查在入队之前被调用,所以一个新获取的线程可能会 插队 到其他已阻塞和排队的线程之前。然而,如果您愿意,可以通过在
tryAcquire
和/或tryAcquireShared
中内部调用一个或多个检查方法来定义插队行为,从而提供一个 公平 的 FIFO 获取顺序。特别是,大多数公平同步器可以定义tryAcquire
,如果hasQueuedPredecessors
(一个专门为公平同步器设计的方法)返回true
,则返回false
。还有其他变体是可能的。吞吐量和可扩展性通常在默认的插队(也称为 贪婪、放弃 和 避免车队)策略下最高。虽然这不能保证公平或无饥饿,但较早排队的线程在较晚排队的线程之前被允许重新竞争,并且每次重新竞争都有一个对新线程无偏见的成功机会。此外,虽然获取操作在通常意义上不会“自旋”,但它们可能会在阻塞之前多次调用
tryAcquire
并穿插其他计算。这在独占同步只被短暂持有时提供了大部分自旋的好处,而在不持有时则没有大部分自旋的缺点。如果需要,您可以通过在调用获取方法之前进行“快速路径”检查来增强这一点,可能预先检查hasContended
和/或hasQueuedThreads
,以仅在同步器可能不被竞争时才这样做。此类通过将其使用范围专门化为可以依赖
int
状态、获取和释放参数以及内部 FIFO 等待队列的同步器,提供了高效和可扩展的同步基础。当这不足以满足需求时,您可以使用java.util.concurrent.atomic
类、自定义java.util.Queue
类和LockSupport
阻塞支持从更低级别构建同步器。以下是一个使用零值表示未锁定状态、一值表示锁定状态的非递归互斥锁类。虽然非递归锁严格来说不需要记录当前所有者线程,但此类还是这样做了,以便更容易监控。它还支持条件并公开了一个工具方法:
class Mutex implements Lock, java.io.Serializable {// 内部辅助类,用于处理同步逻辑private static class Sync extends AbstractQueuedSynchronizer {// 报告当前线程是否独占持有锁protected boolean isHeldExclusively() {return getState() == 1; // 如果状态为1,则表示已锁定}// 尝试获取锁public boolean tryAcquire(int acquires) {assert acquires == 1; // 确保参数为1if (compareAndSetState(0, 1)) { // 如果当前状态为0(未锁定),则将状态设置为1(锁定)setExclusiveOwnerThread(Thread.currentThread()); // 设置当前线程为锁的所有者return true; // 返回成功}return false; // 获取锁失败}// 尝试释放锁protected boolean tryRelease(int releases) {assert releases == 1; // 确保参数为1if (getState() == 0) throw new IllegalMonitorStateException(); // 如果当前状态为0(未锁定),抛出异常setExclusiveOwnerThread(null); // 清除锁的所有者setState(0); // 将状态设置为0(未锁定)return true; // 返回成功}// 创建一个Condition对象Condition newCondition() { return new ConditionObject(); }// 读取对象时重置状态private void readObject(ObjectInputStream s)throws IOException, ClassNotFoundException {s.defaultReadObject(); // 调用默认的读取方法setState(0); // 重置状态为0(未锁定)}}// 内部Sync对象,处理所有同步逻辑private final Sync sync = new Sync();// 实现Lock接口的方法,调用Sync的acquire方法获取锁public void lock() { sync.acquire(1); }// 尝试获取锁,调用Sync的tryAcquire方法public boolean tryLock() { return sync.tryAcquire(1); }// 释放锁,调用Sync的release方法public void unlock() { sync.release(1); }// 创建一个Condition对象public Condition newCondition() { return sync.newCondition(); }// 检查当前线程是否持有锁public boolean isLocked() { return sync.isHeldExclusively(); }// 检查是否有线程在等待队列中public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }// 可中断地获取锁public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}// 在指定时间内尝试获取锁public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));} }
以下是一个类似于 java.util.concurrent.CountDownLatch 的锁存器类,但它只需要一个 signal 即可触发。因为锁存器是非独占的,所以它使用共享的获取和释放方法:
class BooleanLatch {// 内部辅助类,用于处理同步逻辑private static class Sync extends AbstractQueuedSynchronizer {// 检查是否已触发boolean isSignalled() { return getState() != 0; }// 尝试以共享模式获取锁protected int tryAcquireShared(int ignore) {return isSignalled() ? 1 : -1; // 如果已触发,返回1;否则返回-1}// 尝试以共享模式释放锁protected boolean tryReleaseShared(int ignore) {setState(1); // 设置状态为1(已触发)return true; // 返回成功}}// 内部Sync对象,处理所有同步逻辑private final Sync sync = new Sync();// 检查是否已触发public boolean isSignalled() { return sync.isSignalled(); }// 触发锁存器public void signal() { sync.releaseShared(1); }// 等待锁存器触发public void await() throws InterruptedException {sync.acquireSharedInterruptibly(1);} }
核心思想
AQS 的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁(Craig, Landin, and Hagersten locks)进一步优化实现的。
组成部分
AQS 使用一个 volatile int state 变量来表示同步状态,并通过一个内置的 FIFO 线程等待队列来完成获取资源线程的排队工作。state 变量由 volatile 修饰,用于展示当前临界资源的获取情况。状态信息 state 可以通过 protected 类型的 getState()、setState() 和 compareAndSetState() 进行操作。
AQS 中的 CLH 变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中。CLH 变体队列和原本的 CLH 锁队列的区别主要有两点:
-
由 自旋 优化为 自旋 + 阻塞:自旋操作的性能很高,但大量的自旋操作比较占用 CPU 资源,因此在 CLH 变体队列中会先通过自旋尝试获取锁,如果失败再进行阻塞等待。
-
由 单向队列 优化为 双向队列:在 CLH 变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了 next 指针,成为了双向队列。
简单梳理一下,首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象。其核心源码如下:
static final class Node {/** 标记节点以指示其正在以共享模式等待 */static final Node SHARED = new Node();/** 标记节点以指示其正在以独占模式等待 */static final Node EXCLUSIVE = null;/** waitStatus 值,指示线程已取消 */static final int CANCELLED = 1;/** waitStatus 值,指示后继节点的线程需要被唤醒 */static final int SIGNAL = -1;/** waitStatus 值,指示线程正在等待条件 */static final int CONDITION = -2;/*** waitStatus 值,指示下一次 acquireShared 操作应该无条件传播*/static final int PROPAGATE = -3;/*** 状态字段,取值仅为以下几种:* SIGNAL: 此节点的后继节点(或将很快)被阻塞(通过 park),因此当前节点在释放或取消时必须唤醒其后继节点。* 为避免竞争,获取方法必须先表明需要信号,然后重试原子获取操作,失败后再阻塞。* CANCELLED: 由于超时或中断,此节点已取消。节点永远不会离开此状态。* 特别地,具有取消节点的线程永远不会再次阻塞。* CONDITION: 此节点当前位于条件队列上。在转移之前,它不会用作同步队列节点,* 转移时状态将设置为 0。(此处使用此值与该字段的其他用途无关,但简化了机制。)* PROPAGATE: 一个 releaseShared 操作应该传播到其他节点。这仅在 doReleaseShared 中为头节点设置,* 以确保即使其他操作介入,传播也能继续。* 0: 以上情况均不满足** 这些值按数字顺序排列以简化使用。非负值表示节点不需要信号。* 因此,大多数代码不需要检查特定值,只需检查符号。** 该字段对于普通同步节点初始化为 0,对于条件节点初始化为 CONDITION。* 它使用 CAS(或在可能的情况下,无条件的 volatile 写入)进行修改。*/volatile int waitStatus;/*** 指向当前节点/线程依赖的前驱节点的链接,用于检查 waitStatus。* 在入队时分配,仅在出队时为了垃圾回收而置为 null。* 此外,当前驱节点取消时,我们会短路查找一个未取消的节点,* 因为头节点永远不会取消:节点只有在成功获取时才会成为头节点。* 取消的线程永远不会成功获取,并且线程只会取消自身,不会取消其他节点。*/volatile Node prev;/*** 指向当前节点/线程在释放时唤醒的后继节点的链接。* 在入队时分配,在绕过取消的前驱节点时调整,在出队时为了垃圾回收而置为 null。* 入队操作在附加后才会分配前驱节点的 next 字段,因此看到 null 的 next 字段并不一定意味着节点在队列末尾。* 然而,如果 next 字段看起来为 null,我们可以从尾部扫描 prev 来双重检查。* 取消节点的 next 字段设置为指向自身而不是 null,以便于 isOnSyncQueue 方法的实现。*/volatile Node next;/*** 入队此节点的线程。在构造时初始化,使用后置为 null。*/volatile Thread thread;/*** 指向等待条件的下一个节点的链接,或特殊值 SHARED。* 由于条件队列仅在独占模式下访问,我们只需要一个简单的链表来持有等待条件的节点。* 然后将它们转移到队列中重新获取。由于条件只能是独占的,* 我们通过使用特殊值来表示共享模式,节省了一个字段。*/Node nextWaiter;/*** 检查节点是否以共享模式等待。** @return 如果节点以共享模式等待,则返回 true;否则返回 false。*/final boolean isShared() {// 通过比较 nextWaiter 是否等于 SHARED 来判断节点是否以共享模式等待return nextWaiter == SHARED;}/*** 返回前驱节点,如果前驱节点为 null 则抛出 NullPointerException。* 当确定前驱节点不为 null 时使用此方法。虽然可以省略 null 检查,但为了帮助虚拟机进行优化而保留。** @return 此节点的前驱节点* @throws NullPointerException 如果前驱节点为 null*/final Node predecessor() throws NullPointerException {// 获取前驱节点Node p = prev;// 检查前驱节点是否为 nullif (p == null)// 若为 null 则抛出异常throw new NullPointerException();else// 否则返回前驱节点return p;}/*** 用于建立初始头节点或共享标记的构造函数。*/Node() { // Used to establish initial head or SHARED marker}/*** 由 addWaiter 方法使用的构造函数。** @param thread 入队此节点的线程* @param mode 节点的模式,SHARED 或 EXCLUSIVE*/Node(Thread thread, Node mode) { // 初始化 nextWaiter 为传入的模式this.nextWaiter = mode;// 初始化 thread 为传入的线程this.thread = thread;}/*** 由 Condition 类使用的构造函数。** @param thread 入队此节点的线程* @param waitStatus 节点的等待状态*/Node(Thread thread, int waitStatus) { // 初始化 waitStatus 为传入的等待状态this.waitStatus = waitStatus;// 初始化 thread 为传入的线程this.thread = thread;}}
工作原理
AQS 的工作原理基于队列和状态两个重要概念。它使用一个 state 变量来记录同步状态,并通过一个 FIFO 队列来管理等待的线程。
具体流程如下:
-
状态管理:AQS 通过 state 变量来表示同步器的状态,通常是一个整数。该状态用于表示资源的可用性,比如锁是否已经被占用。
-
线程队列:当线程不能获取到资源时,它会被加入到一个等待队列中。等待队列中的线程会按顺序等待,直到资源释放。
-
CAS 操作:AQS 底层依赖于 CAS(Compare and Swap)操作来实现原子性的状态更新,保证线程安全。
优点
-
高效的线程排队机制:AQS 使用一个 FIFO 队列来管理线程,保证了线程按照请求的顺序来获取资源,避免了线程饿死的情况。
-
支持自定义同步器:通过继承
AbstractQueuedSynchronizer
,我们可以非常方便地实现各种自定义的同步工具,如锁、信号量、栅栏等。 -
底层使用 CAS 操作:AQS 底层使用 CAS(Compare And Swap)操作来保证状态更新的原子性,避免了使用传统锁的开销。
缺点与注意事项
-
性能瓶颈:虽然 AQS 提供了无锁的同步机制,但在高并发场景下,如果竞争激烈,仍然可能导致性能下降(例如 CAS 操作的失败需要不断重试)。
-
死锁问题:AQS 在实现自定义同步器时需要非常小心死锁问题的发生,尤其是在锁的嵌套使用时。
AQS中常见的问题
AQS中为什么要有一个虚拟的head节点
AQS可以没有head,设计之初指定head只是为了更方便的操作,其主要作为一个哨兵节点的存在,核心是为了方便管理双向链表。
以下则是这么设计的具体原因:
1.方便管理双向链表
-
AQS 的等待队列是一个双向链表,哨兵节点作为队列的起点,使得整个链表的管理更加清晰和简单。例如,在 ReentrantLock 中,当线程需要获取锁时,如果无法立即获取,就会被封装成一个节点并添加到链表的尾部。哨兵节点的存在可以避免队列为空时的特殊处理,使得所有的节点都可以以统一的方式进行管理。
-
哨兵节点本身不存储任何线程信息,其 Thread 字段为 null,waitStatus 为 0。这使得队列在初始化时就有了一个明确的头节点,避免了在插入第一个节点时需要特殊处理的情况。
2. 优化唤醒操作
-
当锁资源被释放时,需要唤醒等待队列中的下一个线程。哨兵节点的存在使得这个过程更加高效。例如,在 ReentrantLock 中,释放锁时会检查哨兵节点的下一个节点(即实际的第一个等待节点)是否需要被唤醒。如果该节点的状态不是 CANCELLED(表示该线程已被取消),则将其唤醒。
-
如果没有哨兵节点,直接从队列的头节点开始查找需要唤醒的节点,可能会面临头节点被取消或者需要遍历整个链表的情况,增加了操作的复杂性和时间成本。
3. 避免空指针异常
-
在没有哨兵节点的情况下,如果队列为空,直接访问头节点可能会导致空指针异常。哨兵节点的存在可以避免这种情况,因为即使队列中没有其他节点,哨兵节点始终存在,确保了操作的安全性。
-
例如,当一个线程尝试获取锁时,如果队列为空,哨兵节点的存在使得队列的头节点始终存在,可以安全地进行操作,而不需要额外的条件判断。
4. 简化逻辑和提高性能
-
哨兵节点的引入简化了 AQS 的逻辑,使得代码更加清晰和易于维护。例如,在插入新节点时,只需要将新节点的 prev 指针指向链表的最后一个节点,而不需要处理队列为空的特殊情况。
-
同时,哨兵节点的存在也有助于提高性能。例如,在唤醒后继节点时,可以直接从哨兵节点的 next 指针快速定位到第一个需要唤醒的节点,而不需要从头开始遍历链表。
5. 支持取消操作
-
当一个线程被取消或者超时等待时,可以通过设置节点的状态为 CANCELLED 来表示。哨兵节点的存在使得在处理取消操作时,可以方便地跳过被取消的节点,找到下一个有效的节点。
-
例如,当需要唤醒后继节点时,如果直接访问哨兵节点的 next 节点发现其状态为 CANCELLED,则可以继续向后查找,直到找到一个有效的节点。
6. 并发安全性
-
在多线程环境下,哨兵节点的存在有助于维护队列的并发安全性。例如,在插入新节点时,可以使用原子操作来更新链表的尾节点,确保线程安全。
-
哨兵节点的存在使得整个链表的结构更加稳定,减少了并发操作时的竞争和冲突。
AQS中为什么使用双向链表
AQS的双向链表就为了更方便的操作Node节点。在执行tryLock,lockInterruptibly方法时,如果在线程阻塞时,中断了线程,此时线程会执行cancelAcquire取消当前节点,不在AQS的双向链表中排队。如果是单向链表,此时会导致取消节点,无法直接将当前节点的prev节点的next指针,指向当前节点的next节点。
以下则是具体原因:
1. 灵活地管理线程队列
-
高效插入和删除:双向链表允许在任意位置快速插入和删除节点。当线程需要等待锁时,可以将其插入到链表的尾部;当锁被释放时,可以快速从链表中移除相应的线程节点。这种灵活性对于动态变化的线程队列来说非常重要。
-
方便遍历:双向链表既可以从头到尾遍历,也可以从尾到头遍历。这使得在某些情况下,比如需要删除中间节点或处理线程状态变化时,可以更方便地进行操作。
2. 支持线程状态的高效管理
-
线程状态转换:双向链表中的每个节点可以存储线程的状态信息,如 CANCELLED、SIGNALED、CONDITION 等。通过双向链表,可以快速定位和修改这些状态,从而更好地管理线程的等待和唤醒过程。
-
条件变量的使用:在 AQS 中,条件变量(Condition)的实现依赖于双向链表。当线程在条件队列中等待时,可以将其节点插入到链表中;当条件满足时,可以快速唤醒相应的线程节点。
3. 简化队列操作的实现
-
统一的接口:双向链表提供了一组统一的接口,如 addLast、remove、findFirst 等,使得队列操作的实现更加清晰和简洁。这有助于降低代码的复杂性,提高代码的可维护性。
-
减少竞争:双向链表的结构允许对队列进行分段管理,从而减少线程之间的竞争。例如,在 AQS 的实现中,锁的获取和释放操作可以分别在链表的不同部分进行,从而提高并发性能。
4. 支持复杂的同步逻辑
-
公平性和非公平性:双向链表可以灵活地支持公平性和非公平性的锁实现。在公平锁中,线程按照 FIFO(先入先出)的顺序获取锁;在非公平锁中,允许插队的线程优先获取锁。双向链表的灵活性使得这两种情况都可以高效地实现。
-
超时和中断处理:双向链表有助于实现线程的超时等待和中断操作。当线程等待超时或被中断时,可以将其从链表中移除,并进行相应的处理。
关于ReentrantLock
ReentrantLock 是基于 AQS 实现的,它利用了 AQS 提供的队列管理和同步原语,实现了可重入锁的功能。通过继承 AQS,ReentrantLock 可以方便地管理和维护锁的状态,支持公平锁和非公平锁,并提供了高效的线程同步机制。
ReentrantLock是AQS的典型应用案例,两者关系可概括为:
- AQS提供基础设施:状态原子操作、线程队列、阻塞唤醒机制;
- ReentrantLock定制策略:通过继承AQS实现可重入、公平性等具体锁语义。
这种分层设计使得ReentrantLock在保证性能的同时,具备高度扩展性。
ConditionObject
像synchronized提供了wait和notify的方法实现线程在持有锁时,可以实现挂起,已经唤醒的操作。ReentrantLock也拥有这个功能。
ReentrantLock 提供了 Condition 对象(Condition 对象的实现也依赖于 AQS 的内部机制),它的 await 和 signal 方法实现类似wait和notify的功能。当线程调用 await 方法时,它会被加入到一个条件队列中,并释放锁;当线程调用 signal 方法时,它会唤醒条件队列中的一个线程,并将其重新加入到同步队列中。当然,想执行await或者是signal就必须先持有lock锁的资源。
Condition的await方法分析(前置分析)
持有锁的线程在执行await方法后会做几个操作:
- 判断线程是否中断,如果中断了,什么都不做。
- 没有中断,就讲当前线程封装为Node添加到Condition的单向链表中
- 一次性释放掉锁资源。
- 如果当前线程没有在AQS队列,就正常执行LockSupport.park(this)挂起线程。
Condition的signal方法分析
分为了几个部分:
- 确保执行signal方法的是持有锁的线程
- 脱离Condition的队列
- 将Node状态从-2改为0
- 将Node添加到AQS队列
- 为了避免当前Node无法在AQS队列正常唤醒做了一些判断和操作
Conditiond的await方法分析(后置分析)
分为了几个部分:
-
唤醒之后,要先确认是中断唤醒还是signal唤醒,还是signal唤醒后被中断
-
确保当前线程的Node已经在AQS队列中
-
执行acquireQueued方法,等待锁资源。
-
在获取锁资源后,要确认是否在获取锁资源的阶段被中断过,如果被中断过,并且不是THROW_IE,那就确保interruptMode是REINTERRUPT。
-
确认当前Node已经不在Condition队列中了
-
最终根据interruptMode来决定具体做的事情
-
0:啥也不做。
-
THROW_IE:抛出异常
-
REINTERRUPT:执行线程的interrupt方法
-
Condition的awaitNanos&signalAll方法分析
- awaitNanos:仅仅是在await方法的基础上,做了一内内的改变,整体的逻辑思想都是一样的。挂起线程时,传入要阻塞的时间,时间到了,自动唤醒,走添加到AQS队列的逻辑
- signalAll方法:这个方法一看就懂,之前signal是唤醒1个,这个是全部唤醒
ReentrantLock和synchronized的区别
核心区别
ReentrantLock是个类,synchronized是关键字,当然都是在JVM层面实现互斥锁的方式
效率区别
如果竞争比较激烈,推荐ReentrantLock去实现,因为它不存在锁升级的概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。所以相比较起来synchronized的效率会更差。
在大多数简单的同步场景下,synchronized 的性能和 ReentrantLock 相差不大,因为 JVM 对 synchronized 进行了很多优化。
但在竞争激烈、需要更灵活控制锁获取的情况下,ReentrantLock 可能表现更好,因为它的非阻塞获取锁和可中断等待等特性能够避免一些不必要的阻塞。
底层实现区别
实现原理是不一样,ReentrantLock基于AQS实现的,synchronized不同锁是基于不同方式实现的,比如重量级锁是基于ObjectMonitor。
synchronized 是基于 JVM 内置的监视器锁(Monitor)机制实现的,涉及到对象头的标记位以及底层的操作系统互斥量。
ReentrantLock 是基于 Java 代码实现的,其底层依赖于 AbstractQueuedSynchronizer(AQS) 框架。AQS 通过一个 volatile 修饰的整型变量 state 来表示同步状态,以及一个 FIFO(先进先出)的线程等待队列来管理线程的阻塞和唤醒。
功能向的区别
ReentrantLock的功能比synchronized更全面。
- ReentrantLock支持公平锁和非公平锁,synchronized只支持非公平锁
- ReentrantLock可以指定等待锁资源的时间。
- ReentrantLock 功能更加丰富,比如可以实现公平锁和非公平锁的选择、支持等待锁的线程中断、支持设置获取锁的超时时间等。
synchronized 相对功能较简单,不支持这些高级特性。
如何选择
ReentrantLock 和 synchronized 都是 Java 中用于实现线程同步的机制,synchronized 是 Java 语言内置的关键字,使用简单方便,适用于大多数简单的同步需求;而 ReentrantLock 提供了更丰富的功能和更灵活的锁机制,适用于复杂的同步需求。
所以如果需要更高级的功能和灵活性,如可中断的锁获取、超时等待、公平锁等,可以选择 ReentrantLock;如果对性能要求不是特别高,并且希望使用更简单的语法和内置的同步机制,可以选择 synchronized。
相关文章:
Java多线程与高并发专题——从AQS到ReentrantLock
关于AQS AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,…...

力扣 寻找重复数
二分,双指针,环形链表。 题目 不看完题就是排序后,用两个快慢指针移动,找到相同就返回即可。 class Solution {public int findDuplicate(int[] nums) {Arrays.sort(nums);int l0;int r1;while(r<nums.length){if(nums[l]num…...

第48天:Web开发-JavaEE应用依赖项Log4j日志Shiro验证FastJson数据XStream格式
#知识点 1、安全开发-JavaEE-第三方依赖开发安全 2、安全开发-JavaEE-数据转换&FastJson&XStream 3、安全开发-JavaEE-Shiro身份验证&Log4j日志处理 一、Log4j 一个基于Java的日志记录工具,当前被广泛应用于业务系统开发,开发者可以利用该工…...
ES6笔记总结
首先我们需要了解一下什么是 ECMA: ECMA(European Computer Manufacturers Association)中文名称为欧洲计算机制造商协会,这 个组织的目标是评估、开发和认可电信和计算机标准。1994 年后该组织改名为 Ecma 国际 什么是 ECMAScr…...

使用Docker Desktop部署GitLab
1. 环境准备 确保Windows 10/11系统支持虚拟化技术(需在BIOS中开启Intel VT-x/AMD-V)内存建议≥8GB,存储空间≥100GB 2. 安装Docker Desktop 访问Docker官网下载安装包安装时勾选"Use WSL 2 instead of Hyper-V"(推荐…...
经典算法 统计数字问题(常数时间解决)
统计数字问题 一本书的页码从自然数1 开始顺序编码直到自然数n。书的页码按照通常的习惯编排,每个页码都不含多余的前导数字0。例如,第6 页用数字6 表示,而不是06 或006 等。数字计数问题要求对给定书的总页码n,计算出书的全部页…...

基于yolov8的糖尿病视网膜病变严重程度检测系统python源码+pytorch模型+评估指标曲线+精美GUI界面
【算法介绍】 基于YOLOv8的糖尿病视网膜病变严重程度检测系统 基于YOLOv8的糖尿病视网膜病变严重程度检测系统是一款利用深度学习技术,专为糖尿病视网膜病变早期诊断设计的智能辅助工具。该系统采用YOLOv8目标检测模型,结合经过标注和处理的医学影像数…...
AcWing 5933:爬楼梯 ← 递归 / 递推 / 高精度
【题目来源】 https://www.acwing.com/problem/content/5936/ 【题目描述】 树老师爬楼梯,他可以每次走 1 级或者 2 级,输入楼梯的级数,求不同的走法数。 例如:楼梯一共有 3 级,他可以每次都走一级,或者第…...
c++ 中的容器 vector 与数组 array
当初自学 c 与 c 语言时,一直被指针弄的云里雾里。后来 c 中引入了容器,避免了指针。但是,一些教材把容器的章节放在书本中后面的章节,太不合理。应该把这种方便的功能放到前面,这样一些初学者就不会遇到太多生硬难懂的…...

我的世界1.20.1forge模组开发进阶物品(7)——具有动画、3D立体效果的物品
基础的物品大家都会做了对吧?包括武器的释放技能,这次来点难度,让物品的贴图呈现动画效果和扔出后显示3D立体效果,这个3D立体效果需要先学习blockbench,学习如何制作贴图。 Blockbench Blockbench是一个用于创建和编辑三维模型的免费软件,特别适用于Minecraft模型的设计…...
ubuntu22.04安装docker engine
在Ubuntu 22.04上安装Docker Engine可以通过以下步骤完成: 更新系统包索引: sudo apt update安装必要的依赖包: 这些包允许apt通过HTTPS使用仓库。 sudo apt install -y apt-transport-https ca-certificates curl software-properties-commo…...

性能测试测试策略制定|知名软件测评机构经验分享
随着互联网产品的普及,产品面对的用户量级也越来越大,能抗住指数级增长的瞬间访问量以及交易量是保障购物体验是否顺畅的至关重要的一环,而我们的性能测试恰恰也是为此而存在的。 性能测试是什么呢?性能测试要怎么测呢?…...
Let‘s Encrypt免费证书的应用示例
文章目录 前言证书申请证书介绍cert.pemchain.pemfullchain.pemprivkey.pem 使用步骤搭建简易demo应用新建nginx配置文件测试SSL是否生效 总结 前言 最近在搞苹果应用上架的问题,据说用HTTP会被拒,但貌似不绝对,2017年苹果曾发公告说必须要求…...

threeJS——安装以及三要素
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、安装二、三要素1.场景1.1创建场景1.2向场景添加元素1.3场景属性 2.相机2.1相机特点2.2正交相机2.3空间布局2.4小姐操作 3.渲染器 总结 前言 本章简单介绍前…...
【Electron入门】进程环境和隔离
目录 一、主进程和渲染进程 1、主进程(main) 2、渲染进程(renderer) 二、预加载脚本 三、沙盒化 为单个进程禁用沙盒 全局启用沙盒 四、环境访问权限控制:contextIsolation和nodeIntegration 1、contextIsola…...
提示词框架介绍和使用场景
框架介绍 CO-STAR 框架 定义 CO-STAR是六个关键要素的缩写,每个字母代表一个特定的部分: Context(上下文) :提供任务的背景信息或环境 当前任务是为一家科技公司撰写一篇关于人工智能发展趋势的文章/ 需要为一场面向高中生的科普讲座准备内容Objective(目标) :明确任…...

牛客NC288803 和+和
import java.util.Comparator;import java.util.PriorityQueue;import java.util.Scanner;public class Main {public static void main(String[] args) {// 创建Scanner对象用于读取输入Scanner sc new Scanner(System.in);// 读取两个整数n和m,分别表示数组的…...
AI学习第七天
数组:基础概念、存储特性及力扣实战应用 在计算机科学与数学的广袤领域中,数组作为一种极为重要的数据结构,发挥着不可或缺的作用。它就像一个有序的 “数据仓库”,能高效地存储和管理大量数据。接下来,让我们深入了解…...

【uniapp原生】实时记录接口请求延迟,并生成写入文件到安卓设备
在开发实时数据监控应用时,记录接口请求的延迟对于性能分析和用户体验优化至关重要。本文将基于 UniApp 框架,介绍如何实现一个实时记录接口请求延迟的功能,并深入解析相关代码的实现细节。 前期准备&必要的理解 1. 功能概述 该功能的…...
XR应用测试:探索虚拟与现实的边界
引言 随着XR(扩展现实,Extended Reality)技术的快速发展,VR(虚拟现实)、AR(增强现实)和MR(混合现实)应用逐渐渗透到游戏、教育、医疗、工业等多个领域。对于…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

SCAU期末笔记 - 数据分析与数据挖掘题库解析
这门怎么题库答案不全啊日 来简单学一下子来 一、选择题(可多选) 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘:专注于发现数据中…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...
服务器--宝塔命令
一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行! sudo su - 1. CentOS 系统: yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...

nnUNet V2修改网络——暴力替换网络为UNet++
更换前,要用nnUNet V2跑通所用数据集,证明nnUNet V2、数据集、运行环境等没有问题 阅读nnU-Net V2 的 U-Net结构,初步了解要修改的网络,知己知彼,修改起来才能游刃有余。 U-Net存在两个局限,一是网络的最佳深度因应用场景而异,这取决于任务的难度和可用于训练的标注数…...

tauri项目,如何在rust端读取电脑环境变量
如果想在前端通过调用来获取环境变量的值,可以通过标准的依赖: std::env::var(name).ok() 想在前端通过调用来获取,可以写一个command函数: #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...