40 张图,精通 Java AQS
Java中 AQS 是 AbstractQueuedSynchronizer 类AQS 依赖 FIFO 队列来提供一个框架这个框架用于实现锁以及锁相关的同步器比如信号量、事件等。在 AQS 中主要有两部分功能一部分是操作 state 变量第二部分是实现排队和阻塞机制。注意AQS 并没有实现任何同步接口它只是提供了类似 acquireInterruptible 的方法调用这些方法可以实现锁和同步器。1 管程模型Java 使用 MESA 管程模型来管理类的成员变量和方法让这个类的成员变量和方法的操作是线程安全的。下图是 MESA 管程模型里面除了定义共享变量外还定义了条件变量和条件变量等待队列上图中有三个知识点MESA 管程模型封装了共享变量和对共享变量的操作线程要进入管程内部必须获取到锁如果获取锁失败就进入入口等待队列阻塞等待。如果线程获取到锁就进入到管程内部。但是进入到管程内部也不一定能立刻操作共享变量而是要看条件变量是否满足如果不满足只能进入条件变量等待队列阻塞等待。在条件变量等待队列中如果被其他线程唤醒也不一定能立刻操作共享变量而是需要去入口等待队列重新排队等待获取锁。Java 中的 MESA 管程模型有一点改进就是管程内部只有一个条件变量和一个等待队列。下图是 AQS 的管程模型AQS 的管程模型依赖 AQS 中的 FIFO 队列实现入口等待队列要进入管程内部就由各种并发锁的限制。而 ConditionObject 则实现了条件队列这个队列可以创建多个。下面就从入口等待队列、并发锁、条件等待队列三个方面来带你彻底理解 AQS。2 入口等待队列2.1 获取独占锁独占, 忽略 interruptspublic final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }这里的 tryAcquire 是抽象方法由 AQS 的子类来实现因为每个子类实现的锁是不一样的。2.1.1 入队上面的代码可以看到获取锁失败后会先执行 addWaiter 方法加入队列然后执行 acquireQueued 方法自旋地获取锁直到成功。addWaiter 代码逻辑如下图简单说就是把 node 入队入队后返回 node 参数给 acquireQueued 方法这里有一个点需要注意如果队列为空则新建一个 Node 作为队头。2.1.2 入队后获取锁acquireQueued 自旋获取锁逻辑如下图这里有几个细节1.waitStatusCANCELLED(1)当前节点取消获取锁。当等待超时或被中断(响应中断)会触发变更为此状态进入该状态后节点状态不再变化SIGNAL(-1)后面节点等待当前节点唤醒CONDITION(-2)Condition 中使用当前线程阻塞在 Condition如果其他线程调用了 Condition 的 signal 方法这个结点将从等待队列转移到同步队列队尾等待获取同步锁PROPAGATE(-3)共享模式前置节点唤醒后面节点后唤醒操作无条件传播下去0中间状态当前节点后面的节点已经唤醒但是当前节点线程还没有执行完成。2.获取锁失败后挂起如果前置节点不是头节点或者前置节点是头节点但当前节点获取锁失败这时当前节点需要挂起分三种情况前置节点 waitStatus-1如下图前置节点 waitStatus 0如下图前置节点 waitStatus 0 但不等于 -1如下图3.取消获取锁如果获取锁抛出异常则取消获取锁如果当前节点是 tail 节点分两种情况如下图如果当前节点不是 tail 节点也分两种情况如下图4.对中断状态忽略5.如果前置节点的状态是 0 或 PROPAGATE会被当前节点自旋过程中更新成 -1以便之后通知当前节点。2.1.3 独占 响应中断对应方法 acquireInterruptibly(int arg)。跟忽略中断(acquire方法)不同的是要响应中断下面两个地方响应中断获取锁之前会检查当前线程是否中断。获取锁失败入队在队列中自旋获取锁的过程中也会检查当前线程是否中断。如果检查到当前线程已经中断则抛出 InterruptedException当前线程退出。2.1.4 独占 响应中断 考虑超时对应方法 tryAcquireNanos(int arg, long nanosTimeout)。这个方法具备了独占 响应中断 超时的功能下面2个地方要判断是否超时自旋获取锁的过程中每次获取锁失败都要判断是否超时;获取锁失败 park 之前要判断超时时间是否大于自旋的阈值时间 (spinForTimeoutThreshold 1ns) 另外park 线程的操作使用 parkNanos 传入阻塞时间。2.2 释放独占锁独占锁释放分两步释放锁唤醒后继节点。释放锁的方法 tryRelease 是抽象的由子类去实现。我们看一下唤醒后继节点的逻辑首先需要满足两个条件head 节点不等于 null;head 节点 waitStatus 不等于 0。这里有两种情况(在方法 unparkSuccessor)情况一后继节点 waitStatus 0直接唤醒后继节点如下图情况二后继节点为空或者 waitStatus 0从后往前查找最接近当前节点的节点进行唤醒如下图2.3 获取共享锁之前我们讲了独占锁这一小节我们谈共享锁有什么不同呢2.3.1 共享忽略 interrupts对应方法 acquireShared代码如下public final void acquireShared(int arg) { if (tryAcquireShared(arg) 0) doAcquireShared(arg); }2.3.2 tryAcquireShared这里获取锁使用的方法是 tryAcquireShared获取的是共享锁。获取共享锁跟获取独占锁不同的是会返回一个整数值说明如下:返回负数获取锁失败。返回 0获取锁成功但是之后再由线程来获取共享锁时就会失败。返回正数:获取锁成功而且之后再有线程来获取共享锁时也可能会成功。所以需要把唤醒操作传播下去。tryAcquireShared 获取锁失败后(返回负数)就需要入队后自旋获取也就是执行方法 doAcquireShared。2.3.3 doAcquireShared怎么判断队列中等待节点是在等待共享锁呢nextWaiter SHARED这个参数值是入队新建节点的时候构造函数传入的。自旋过程中如果获取锁成功(返回正数)首先把自己设置成新的 head 节点然后把通知传播下去。如下图之后会唤醒后面节点并保证唤醒操作可以传播下去。但是需要满足四个条件中的一个tryAcquireShared 返回值大于0有多余的锁可以继续唤醒后继节点。旧的 head 节点 waitStatus 0应该是其他线程释放共享锁过程中把它的状态更新成了 -3。新的 hade 节点 waitStatus 0只要不是 tail 节点就可能是 -1。这里会造成不必要的唤醒因为唤醒后获取不到锁只能继续入队等待。当前节点的后继节点是空或者非空但正在等待共享锁。唤醒后面节点的操作其实就是释放共享锁对应方法是 doReleaseShared见释放共享锁一节。2.3.4 共享 响应中断对应方法 acquireSharedInterruptibly(int arg)。跟共享忽略中断(acquireShared 方法)不同的是要响应中断下面两个地方响应中断获取锁之前会检查当前线程是否中断。获取锁失败入队在队列中自旋获取锁的过程中也会检查当前线程是否中断。如果检查到当前线程已经中断则抛出 InterruptedException当前线程退出。2.3.5 共享 响应中断 考虑超时对应方法 tryAcquireSharedNanos(int arg, long nanosTimeout)。这个方法具备了共享 响应中断 超时的功能下面两个个地方要判断是否超时自旋获取锁的过程中每次获取锁失败都要判断是否超时。获取锁失败 park 之前要判断超时时间是否大于自旋的阈值时间(spinForTimeoutThreshold 1ns)。另外park 线程的操作使用 parkNanos 传入阻塞时间。2.4 释放共享锁释放共享锁代码如下public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }首先尝试释放共享锁tryReleaseShared 代码由子类来实现。释放成功后执行AQS中的 doReleaseShared 方法是一个自旋操作。自旋的条件是队列中至少有两个节点这里分三种情况。情况一当前节点 waitStatus 是 -1如下图情况二当前节点 waitStatus 是 0(被其他线程更新新成了中间状态)如下图情况三当前节点 waitStatus 是 -3为什么会这样呢需要解释一下head节点唤醒后继节点之前 waitStatus 已经被更新中间态 0 了唤醒后继节点动作还没有执行又被其他线程更成了 -3也就是其他线程释放锁执行了上面情况二。这时需要先把 waitStatus 再更成 0 (在方法 unparkSuccessor)如下图2.5 抽象方法上面的讲解可以看出如果要基于 AQS 来实现并发锁可以根据需求重写下面四个方法来实现这四个方法在 AQS 中没有具体实现tryAcquire(int arg)获取独占锁tryRelease(int arg)释放独占锁tryAcquireShared(int arg)获取共享锁tryReleaseShared(int arg)释放共享锁AQS 的子类需要重写上面的方法来修改 state 值并且定义获取锁或者释放锁时 state 值的变化。子类也可以定义自己的 state 变量但是只有更新 AQS 中的 state变量才会对同步起作用。还有一个判断当前线程是否持有独占锁的方法 isHeldExclusively也可以供子类重写后使用。获取/释放锁的具体实现放到下篇文章讲解。2.6 总结AQS 使用 FIFO 队列实现了一个锁相关的并发器模板可以基于这个模板来实现各种锁包括独占锁、共享锁、信号量等。AQS 中有一个核心状态是 waitStatus这个代表节点的状态决定了当前节点的后续操作比如是否等待唤醒是否要唤醒后继节点。3 并发锁这一章节讲解 Java AQS 中的并发锁。其实 Java AQS 中的并发锁主要是基于 state 这个变量值来实现的。3.1 ReentrantLock我们先来看一下 UML 类图从图中可以看到ReentrantLock 使用抽象内部类 Sync 来实现了 AQS 的方法然后基于 Sync 这个同步器实现了公平锁和非公平锁。主要实现了下面 3 个方法tryAcquire(int arg)获取独占锁tryRelease(int arg)释放独占锁isHeldExclusively当前线程是否占有独占锁。ReentrantLock 默认实现的是非公平锁可以在构造函数指定。从实现的方法可以看到ReentrantLock 中获取的锁是独占锁我们再来看一下获取和释放独占锁的代码public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }独占锁的特点是调用上面 acquire 方法传入的参数是 1。3.1.1 获取公平锁获取锁首先判断同步状态(state)的值。3.1.1.1 state 等于 0这说明没有线程占用锁当前线程如果符合下面两个条件就可以获取到锁没有前任节点如下图CAS 的方式更新 state 值(把 0 更新成 1)成功。如果获取独占锁成功会更新 AQS 中 exclusiveOwnerThread 为当前线程这个很容易理解。3.1.1.2 state 不等于 0这说明已经有线程占有锁判断占有锁的线程是不是当前线程如下图state 1 值如果小于 0会抛出异常。如果获取锁失败则进入 AQS 队列等待唤醒。3.1.2 获取非公平锁跟公平锁相比非公平锁的唯一不同是如果判断到 state 等于 0不用判断有没有前任节点只要 CAS 设置 state 值(把 0 更新成 1)成功就获取到了锁。3.1.3 释放锁公平锁和非公平锁释放逻辑完全一样都是在内部类 Sync 中实现的。释放锁需要注意两点如下图为什么 state 会大于 1因为是可以重入的占有锁的线程可以多次获取锁。3.1.4 总结公平锁的特点是每个线程都要进行排队不用担心线程永远获取不到锁但有个缺点是每个线程入队后都需要阻塞和被唤醒这一定程度上影响了效率。非公平锁的特点是每个线程入队前都会先尝试获取锁如果获取成功就不会入队了这比公平锁效率高。但也有一个缺点队列中的线程有可能等待很长时间高并发下甚至可能永远获取不到锁。3.2 ReentrantReadWriteLock我们先来看一下 UML 类图从图中可以看到ReentrantReadWriteLock 使用抽象内部类Sync来实现了 AQS 的方法然后基于 Sync 这个同步器实现了公平锁和非公平锁。主要实现了下面 3 个方法tryAcquire(int arg)获取独占锁tryRelease(int arg)释放独占锁tryAcquireShared(int arg)获取共享锁tryReleaseShared(int arg)释放共享锁isHeldExclusively当前线程是否占有独占锁 可见ReentrantReadWriteLock里面同时用到了共享锁和独占锁。下图是定义的几个常用变量下面这 2 个方法用户获取共享锁和独占锁的数量static int sharedCount(int c) { return c SHARED_SHIFT; } static int exclusiveCount(int c) { return c EXCLUSIVE_MASK; }从sharedCount 可以看到共享锁的数量要右移 16 位获取也就是说共享锁占了高 16 位。从上图 EXCLUSIVE_MASK 的定义看到跟 EXCLUSIVE_MASK 进行与运算得到的是低 16 位的值所以独占锁占了低 16 位。如下图这样上面获取锁数量的方法就很好理解了。3.2.1 读锁读锁的实现对应内部类 ReadLock。3.2.1.1 获取读锁获取读锁实际上是 ReadLock 调用了 AQS 的下面方法,传入参数是 1public final void acquireShared(int arg) { if (tryAcquireShared(arg) 0) doAcquireShared(arg); }ReentrantReadWriteLock 内部类 Sync 实现了 tryAcquireShared 方法主要包括如下三种情况使用 exclusiveCount 方法查看 state 中是否有独占锁如果有并且独占线程不是当前线程返回 -1获取失败;使用 sharedCount 查看 state 中共享锁数量如果读锁数量小于最大值(MAX_COUNT65535)则再满足下面 3 个条件就可以获取成功并返回 1a.当前线程不需要阻塞(readerShouldBlock)。在公平锁中需要判断是否有前置节点如下图就需要阻塞:在非公平锁中则是判断第一个节点是不是有独占锁如下图就需要阻塞:b.使用 CAS 把 state 的值加 SHARED_UNIT(65536)。这里是不是就更理解读锁占高位的说法了获取一个读锁state 的值就要加 SHARED_UNIT 这么多个。c.给当前线程的 holdCount 加 1。如果 2 失败自旋重复上面的步骤直到获取到锁。tryAcquireShared (获取共享锁)会返回一个整数如下返回负数获取锁失败。返回 0获取锁成功但是之后再由线程来获取共享锁时就会失败。返回正数:获取锁成功而且之后再有线程来获取共享锁时也可能会成功。3.2.1.2 释放读锁ReentrantReadWriteLock 释放读锁是在 ReadLock 中调用了 AQS 下面方法传入的参数是1public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }ReentrantReadWriteLock 内部类 Sync 实现了 releaseShared 方法具体逻辑分为下面两步当前线程 holdCounter 值减 1。CAS的方式将 state 的值减去 SHARED_UNIT。3.2.2 写锁写锁的实现对应内部类 WriteLock。3.2.2.1 获取写锁ReentrantReadWriteLock 获取写锁其实是在 WriteLock 中调用了 AQS 的下面方法传入参数 1public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }在ReentrantReadWriteLock 内部类 Sync 实现了 tryAcquire 方法首先获取 state 值和独占锁数量(exclusiveCount),之后分如下两种情况如下图1.state 不等于 0:独占锁数量等于 0这时说明有线程占用了共享锁如果当前线程不是独占线程获取锁失败。独占锁数量不等于 0独占锁数量加 1 后大于 MAX_COUNT获取锁失败。上面 2 种情况不符合获取锁成功state 值加 1。2.state 等于 0判断当前线程是否需要阻塞(writerShouldBlock)。在公平锁中跟 readerShouldBlock 的逻辑完全一样就是判断队列中 head 节点的后继节点是不是当前线程。在非公平锁中直接返回 false即可以直接尝试获取锁。如果当前线程不需要阻塞并且给 state 赋值成功使用 CAS 方式把 state 值加 1把独占线程置为当前线程。3.2.2.2 释放写锁ReentrantReadWriteLock 释放写锁其实是在 WriteLock 中调用了 AQS 的下面方法传入参数 1public final boolean release(int arg) { if (tryRelease(arg)) { Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); return true; } return false; }ReentrantReadWriteLock 在 Sync 中实现了 tryRelease(arg) 方法逻辑如下判断当前线程是不是独占线程如果不是抛出异常。state值减1后用新state值判断独占锁数量是否等于0如果等于0则把独占线程置为空返回true这样上面的代码就可以唤醒队列中的后置节点了如果不等于0返回false不唤醒后继节点。3.3 CountDownLatch我们先来看一下UML类图从上面的图中看出CountDownLatch 的内部类 Sync 实现了获取共享锁和释放共享锁的逻辑。使用 CountDownLatch 时构造函数会传入一个 int 类型的参数 count表示调动 count 次的 countDown 后主线程才可以被唤醒。public CountDownLatch(int count) { if (count 0) throw new IllegalArgumentException(count 0); this.sync new Sync(count); }上面的 Sync(count) 就是将 AQS 中的 state 赋值为 count。3.3.1 awaitCountDownLatch 的 await 方法调用了 AQS 中的 acquireSharedInterruptibly(int arg)传入参数 1不过这个参数并没有用。代码如下public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) 0) doAcquireSharedInterruptibly(arg); }Sync 中实现了 tryAcquireShared 方法await 逻辑如下图上面的自旋过程就是等待 state 的值不断减小只有 state 值成为 0 的时候主线程才会跳出自旋执行之后的逻辑。3.3.2 countDownCountDownLatch 的 countDown 方法调用了 AQS 的 releaseShared(int arg)传入参数 1不过这个参数并没有用。内部类 Sync 实现了 tryReleaseShared 方法逻辑如下图3.3.3 总结CountDownLatch 的构造函数入参值会赋值给 state 变量入队操作是主线程入队每个子线程调用了countDown 后 state 值减 1当 state 值成为 0 后唤醒主线程。3.4 SemaphoreSemaphore 是一个信号量用来保护共享资源。如果线程要访问共享资源首先从 Semaphore 获取锁(信号量)如果信号量的计数器等于 0则当前线程进入 AQS 队列阻塞等待。否则线程获取锁成功信号量减 1。使用完共享资源后释放锁(信号量加 1)。Semaphore 跟管程模型不一样的是允许多个(构造函数的 permits)线程进入管程内部因此也常用它来做限流。UML 类图如下Semaphore的构造函数会传入一个int类型参数用来初始化state的值。3.4.1 acquire获取锁的操作调用了 AQS 中的 acquireSharedInterruptibly 方法传入参数 1代码见 CountDownLatch 中 await 小节。Semaphore 在公平锁和非公平锁中分别实现了 tryAcquireShared 方法。3.4.1.1 公平锁Semaphore 默认使用非公平锁如果使用公平锁需要在构造函数指定。获取公平锁逻辑比较简单如下图3.4.1.2 非公平锁acquire 在非公平的锁唯一的区别就是不会判断 AQS 队列是否有前置节点(hasQueuedPredecessors)而是直接尝试获取锁。除了 acquire 方法外还有其他几个获取锁的方法原理类似只是调用了 AQS 中的不同方法。3.4.2 release释放锁的操作调用了 AQS 中的 releaseShared(int arg) 方法传入参数 1在内部类 Sync 中实现了 tryReleaseShared 方法逻辑很简单使用 CAS 的方式将 state 的值加 1之后唤醒队列中的后继节点。3.5 ThreadPoolExecutorThreadPoolExecutor 中也用到了 AQS看下面的 UML 类图Worker 主要在 ThreadPoolExecutor 中断线程的时候使用。Worker 自己实现了独占锁在中断线程时首先进行加锁中断操作后释放锁。按照官方说法这里不直接使用 ReentrantLock 的原因是防止调用控制线程池的方法(类似 setCorePoolSize)时能够重新获取到锁3.5.1 tryAcquire使用 CAS 的方式把 AQS 中 state 从 0 改为 1把当前线程置为独占线程。3.5.2 tryRelease把独占线程置为空把 AQS 中 state 改为 0。Worker 初始化的时候会把 state 置为 -1这样是不能获取锁成功的。只有调用了 runWorker 方法才会通过释放锁操作把 state 更为 0。这样保证了只中断运行中的线程而不会中断等待中的线程。3.6 总结AQS 基于双向队列实现了入口等待队列基于 state 变量实现了各种并发锁上篇文章讲了入口等待队列而这篇文章主要讲了基于 AQS 的并发锁原理。4 条件变量等待队列本章节主要讲解管程模型中条件变量等待队列。4.1 官方示例首先我们看一下官方给出的示例代码public class BoundedBuffer { final Lock lock new ReentrantLock(); final Condition notFull lock.newCondition(); final Condition notEmpty lock.newCondition(); final Object[] items new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count items.length) notFull.await(); items[putptr] x; if (putptr items.length) putptr 0; count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count 0) notEmpty.await(); Object x items[takeptr]; if (takeptr items.length) takeptr 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }这个代码定义了两个条件变量notFull 和 notEmpty说明如下如果 items 数组已经满了则 notFull 变量不满足线程需要进入 notFull 条件等待队列进行等待。当 take 方法取走一个数组元素时notFull 条件满足了唤醒 notFull 条件等待队列中等待线程。如果 items 数组为空则 notEmpty 变量不满足线程需要进入 notEmpty 条件等待队列进行等待。当 put 方法加入一个数组元素时notEmpty 条件满足了唤醒 notEmpty 条件等待队列中等待线程。条件变量是绑定在 Lock 上的示例代码使用了 ReentrantLock。在执行 await 和 signal 方法时首先要获取到锁。4.2 原理简介Java AQS 的条件变量等待队列是基于接口 Condition 和 ConditionObject 来实现的URM 类图如下Condition 接口主要定义了下面3个方法await进入条件等待队列signal唤醒条件等待队列中的元素signalAll唤醒条件等待队列中的所有元素4.3 await条件等待队列跟入口等待队列有两个不同虽然二者共用了 Node 类但是条件等待队列是单向队列入口等待队列是双向队列条件队列中下一个节点的引用是 nextWaiter入口等待队列中下一个节点的引用是 next。条件等待队列中元素的 waitStatus 必须是 -2。await 方法的流程如下图4.3.1 进入条件等待队列入队方法对应方法 addConditionWaiter这里有三种情况队列为空则新建一个节点如下图队列非空最后一个元素的 waitStatus 是 -2如下图队列非空最后一个元素的 waitStatus 不是 -2如下图可以看到这种情况会从队列第一个元素开始检查 waitStatus 不是 -2 的元素并从队列中移除。4.3.2 释放锁AQS 的并发锁是基于 state 变量实现的线程进入条件等待队列后要释放锁即 state 会变为 0释放操作会唤醒入口等待队列中的线程。对应方法 fullyRelease返回值是释放锁减掉的 state 值 savedState。4.3.3 阻塞等待释放锁后线程阻塞自旋等待被唤醒。4.3.4 唤醒之后唤醒之后当前线程主要有四个动作转入入口等待队列并把 waitStatus 改为 0。waitStatus 等于 0 表示中间状态当前节点后面的节点已经唤醒但是当前节点线程还没有执行完成。重新获取锁如果获取成功则当前线程成为入口等待队列头结点interruptMode 置为 1。如果当前节点在条件等待队列中有后继节点则剔除条件等待队列中 waitStatus!-2 的节点即队列中状态为取消的节点。interruptMode 如果不等于 0则处理中断。4.3.5 一个细节上面提到了 interruptMode这个属性有三个值0没有被中断-1中断后抛出 InterruptedException这种情况是当前线程阻塞没有被 signal 之前发生了中断1重新进入中断状态这种情况是指当前线程阻塞被 signal 之后发生了中断4.3.6 扩展AQS 还提供了其他几个 await 方法如下awaitUninterruptibly不用处理中断。awaitNanos自旋等待唤醒过程中有超时时间限制超时则转入入口等待队列。awaitUntil自旋等待唤醒过程中有截止时间时间到则转入入口等待队列。4.4 signal唤醒条件等待队列中的元素首先判断当前线程是否持有独占锁如果没有抛出异常。唤醒条件队列中的元素会从第一个元素也就是 firstWaiter 开始根据 firstWaiter 的 waitStatus 是不是 -2分两种情况。4.4.1 waitStatus-2条件队列第一个节点进入入口等待队列等待获取锁如下图这里有两个注意点如果入口等待队列中 tail 节点的 waitStatus 小于等于 0则 firstWaiter 加入后需要把旧 tail 节点置为 -1 (表示后面节点等待当前节点唤醒)如下图如果重置 waitStatus 状态失败,则 unpark 节点 firstWaiter。如果入口等待队列中 tail 节点的 waitStatus 大于 0则 unpark 节点 firstWaiter。4.4.2 waitStatus!-2如果 firstWaiter 的 waitStatus 不等于 -2则查找 firstWaiter 的 nextWaiter直到找到一个 waitStatus 等于 -2 的节点然后将这个节点加入入口等待队列队尾如下图4.4.3 waitStatus 修改上面的两种情况无论哪种进入入口等待队列之前都要用 CAS 的方式把 waitStatus 改为 0。4.5 signalAll理解了 signal 的逻辑signalAll 的逻辑就非常容易理解了。首先判断当前线程是否持有独占锁如果没有抛出异常。将条件等待队列中的所有节点依次加入入口等待队列。如下图4.6 使用案例4.6.1 示例代码Java 并发包下有很多类使用到了 AQS 中的 Condition如下图这里我们以 CyclicBarrier 为例来讲解。CyclicBarrier 是让一组线程相互等待共同达到一个屏障点。从 Cyclic 可以看出 Barrier 可以循环利用也就是当线程释放之后可以继续使用。看下面这段示例代码public static void main(String[] args) { CyclicBarrier cyclicBarrier new CyclicBarrier(2, () - { System.out.println(栅栏中的线程执行完成); }); ExecutorService executorService Executors.newFixedThreadPool(2); executorService.submit(() - { try { System.out.println(线程1 Thread.currentThread().getName()); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } }); executorService.submit(() - { try { System.out.println(线程2 Thread.currentThread().getName()); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } }); executorService.shutdown(); }执行结果线程1pool-1-thread-1 线程2pool-1-thread-2 栅栏中的线程执行完成4.6.2 原理讲解CyclicBarrier 初始化的时候会指定线程的数量 count每个线程执行完逻辑后调用 CyclicBarrier 的 await 方法这个方法首先将 count 减 1然后调用 Condition的 await让当前线程进入条件等待队列。当最后一个线程将 count 减 1 后count 数量等于 0这时就会调用 Condition 的 signalAll 方法唤醒所有线程。4.7 总结Java 的管程模型使用了 MESA 模型基于 AQS 实现的 MESA 模型中使用双向队列实现了入口等待队列使用变量 state 实现了并发锁使用 Condition 实现了条件等待队列。在 AQS 的实现中使用同步队列这个术语来表示双向队列本文中使用入口等待队列来描述是为了更好的配合管程模型来讲解。AQS 的 Condition 中使用 await 方法将当前线程放入条件变量等待队列阻塞等待使用 notify 来唤醒条件等待队列中的线程被唤醒之后线程并不能立刻执行而是进入入口等待队列等待获取锁。

相关新闻

技术面试官视角:除了代码,更看重候选人的哪些潜质与特质?

技术面试官视角:除了代码,更看重候选人的哪些潜质与特质?

技术面试官视角:除了代码,更看重候选人的哪些潜质与特质? 在技术面试中,优秀的代码能力是基础,但面试官往往更关注候选人的底层特质。以下是关键考察维度: 一、问题分析与解决能力 需求拆解能力 能否将模…

2026/7/5 22:58:39 阅读更多 →
数字图像处理篇---高斯模糊

数字图像处理篇---高斯模糊

核心比喻:单反相机拍出的“唯美背景”回忆一下你看过的专业人像照片或电影——人物清晰锐利,但背后的风景、灯光,却化成了一片柔和、朦胧、带有光斑的色块。这就是镜头模糊的效果。如果说高斯模糊是“均匀的毛玻璃”,那镜头模糊就…

2026/7/5 11:12:27 阅读更多 →
C++11实现线程池

C++11实现线程池

这是一个线程池实现,结合了多个异步编程示例。让我详细解释每个部分:整体结构代码展示了多种 C11 异步编程技术的用法,最后实现了一个完整的线程池。各部分详解1. 第一部分:std::async 的基本用法void task(const char* name) {st…

2026/7/5 13:43:14 阅读更多 →

最新新闻

如何从零开始为syzkaller内核模糊测试工具做贡献:新手完全指南

如何从零开始为syzkaller内核模糊测试工具做贡献:新手完全指南

如何从零开始为syzkaller内核模糊测试工具做贡献:新手完全指南 【免费下载链接】syzkaller syzkaller is an unsupervised coverage-guided kernel fuzzer 项目地址: https://gitcode.com/gh_mirrors/sy/syzkaller 你是否曾经想为开源内核安全项目做贡献&…

2026/7/6 19:55:17 阅读更多 →
EhViewer开源项目深度解析:构建现代化漫画浏览体验的Android解决方案

EhViewer开源项目深度解析:构建现代化漫画浏览体验的Android解决方案

EhViewer开源项目深度解析:构建现代化漫画浏览体验的Android解决方案 EhViewer是一个基于Material Design 2设计语言的开源Android漫画浏览应用,为E-Hentai平台用户提供了优雅、高效的内容浏览体验。这个项目不仅解决了漫画爱好者在移动设备上访问和浏览…

2026/7/6 19:55:17 阅读更多 →
FancySelect与iOS设备兼容性:智能适配与强制覆盖技巧

FancySelect与iOS设备兼容性:智能适配与强制覆盖技巧

FancySelect与iOS设备兼容性:智能适配与强制覆盖技巧 【免费下载链接】FancySelect A better select for discerning web developers everywhere. 项目地址: https://gitcode.com/gh_mirrors/fa/FancySelect FancySelect是一个为挑剔的Web开发者精心打造的选…

2026/7/6 19:53:14 阅读更多 →
为什么DocuSeal能让文档签署从繁琐到极简?3分钟了解开源电子签名的未来

为什么DocuSeal能让文档签署从繁琐到极简?3分钟了解开源电子签名的未来

为什么DocuSeal能让文档签署从繁琐到极简?3分钟了解开源电子签名的未来 【免费下载链接】docuseal Open source DocuSign alternative. Create, fill, and sign digital documents ✍️ 项目地址: https://gitcode.com/GitHub_Trending/do/docuseal 想象一下…

2026/7/6 19:53:14 阅读更多 →
PortBender与Cobalt Strike集成指南:打造高效渗透测试工作流

PortBender与Cobalt Strike集成指南:打造高效渗透测试工作流

PortBender与Cobalt Strike集成指南:打造高效渗透测试工作流 【免费下载链接】PortBender TCP Port Redirection Utility 项目地址: https://gitcode.com/gh_mirrors/po/PortBender 在当今复杂的网络安全环境中,渗透测试工具的高效集成成为了红队…

2026/7/6 19:51:13 阅读更多 →
Temboard社区与支持:如何获取PostgreSQL远程控制平台的帮助与资源

Temboard社区与支持:如何获取PostgreSQL远程控制平台的帮助与资源

Temboard社区与支持:如何获取PostgreSQL远程控制平台的帮助与资源 【免费下载链接】temboard PostgreSQL Remote Control 项目地址: https://gitcode.com/gh_mirrors/te/temboard Temboard是一款强大的PostgreSQL远程控制和管理平台,能够帮助数据…

2026/7/6 19:51:13 阅读更多 →

日新闻

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2 与 MySQL 单元测试兼容性:5 个关键 SQL 语句差异与规避方案

H2与MySQL单元测试兼容性:5个关键SQL语句差异与规避方案1. 单元测试中的数据库兼容性挑战在Java开发领域,单元测试是保证代码质量的重要环节。当应用涉及数据库操作时,测试环境的搭建往往成为开发者的痛点。H2数据库因其轻量级、内存模式和快…

2026/7/6 0:01:17 阅读更多 →
Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘

Windows任务栏终极清理指南:用RBTray一键隐藏窗口到系统托盘 【免费下载链接】rbtray A fork of RBTray from http://sourceforge.net/p/rbtray/code/. 项目地址: https://gitcode.com/gh_mirrors/rb/rbtray 你是否厌倦了Windows任务栏上密密麻麻的图标&…

2026/7/6 0:01:17 阅读更多 →
Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C++ 运行时库一键安装终极指南:告别DLL缺失烦恼

Visual C 运行时库一键安装终极指南:告别DLL缺失烦恼 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist 你是否曾经遇到过这样的情况:下载了…

2026/7/6 0:05:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/6 8:11:50 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/6 8:11:52 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/6 6:52:56 阅读更多 →

月新闻