两万字详细解读AQS,你真的了解它吗?

JUC之AQS框架 AQS是一个框架,一个提供锁或同步器依赖于`FIFO等待队列`所必要的“基础设施”的框架。**Dong Lea**之所以写个抽象类的目的是为了简化我们实现同步器的工作。 提供一个基于FIFO等待队列,可以用于构建锁或者其他同步装置的基础框架。意在能够成为实现大部分同步需求的基础。 AQS默认提供了独占式和共享式两种模式,JDK对应的实现有ReentrantLock和ReentrantReadWriteLock。即除 阅读详情
AI摘要

本文详细介绍了Java并发库JUC中的AbstractQueuedSynchronizer(AQS)原理,包括模板方法如acquire和release,以及独占式和共享式获取与释放的实现。AQS通过内部的CLH队列实现线程同步,利用LockSupport工具类进行线程阻塞与唤醒。同时,文章提到了自旋锁的概念,并分析了CountDownLatch和ReentrantLock的底层实现,以及Condition的await和signal机制。通过对AQS的深入理解,有助于更好地掌握Java并发编程。

1、JUC的由来

synchronized 关键字是JDK官方人员用C++代码写的,在JDK6以前是重量级锁。Java大牛 Doug Lea对 synchronized 在并发编程条件下的性能表现不满意就自己写了个JUC,以此来提升并发性能,本文要讲的就是JUC并发包下的AbstractQueuedSynchronizer。

在JUC中 CountDownLatch、ReentrantLock、ThreadPoolExecutor、ReentrantReadWriteLock 等底层用的都是AQS,AQS几乎占据了JUC并发包里的半壁江山,如果想要获取锁可以被中断、超时获取锁、尝试获取锁那就用AQS吧。

DougLea杰作: HashMap、JUC、ConcurrentHashMap等。

温馨提醒:

涉及到AQS重要方法、lock、unlock、CountDownLatch、await、signal几个重要组件的底层讲解所以内容有点长,嫌啰嗦的可直接看几个流程图即可(原图公众号回复lock即可)。

2、AQS前置知识点

2.1、模板方法

AbstractQueuedSynchronizer是个抽象类,所有用到方法的类都要继承此类的若干方法,对应的设计模式就是模版模式。

模版模式定义:一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

抽象类:

public abstract class SendCustom { public abstract void to(); public abstract void from(); public void date() { System.out.println(new Date()); } public abstract void send(); // 注意此处 框架方法-模板方法 public void sendMessage() { to(); from(); date(); send(); } }

模板方法派生类:

public class SendSms extends SendCustom { @Override public void to() { System.out.println("sowhat"); } @Override public void from() { System.out.println("xiaomai"); } @Override public void send() { System.out.println("Send message"); } public static void main(String[] args) { SendCustom sendC = new SendSms(); sendC.sendMessage(); } }

2.2、LookSupport

LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。常用方法如下:

public static void park(Object blocker); // 暂停当前线程 public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制 public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间 public static void park(); // 无期限暂停当前线程 public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制 public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间 public static void unpark(Thread thread); // 恢复当前线程 public static Object getBlocker(Thread t);

叫park是因为park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。

与Object类的wait/notify机制相比,park/unpark有两个优点:

以thread为操作对象更符合阻塞线程的直观定义 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll 唤醒所有等待的线程),增加了灵活性。

park/unpark调用的是 Unsafe(提供CAS操作) 中的 native代码。

park/unpark 功能在Linux系统下是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。mutex和condition保护了一个 _counter 的变量,当 park 时,这个变量被设置为0。当unpark时,这个变量被设置为1。

2.3、CAS

CAS 是 CPU指令级别实现了原子性的比较和交换(Conmpare And Swap)操作,注意CAS不是锁只是CPU提供的一个原子性操作指令。

​CAS在语言层面不进行任何处理,直接将原则操作实现在硬件级别实现,之所以可以实现硬件级别的操作核心是因为CAS操作类中有个核心类UnSafe类。

关于CAS引发的ABA问题、性能开销问题、只能保证一个共享变量之间的原则性操作问题,以前CAS中写过,在此不再重复讲解。

注意:并不是说 CAS 一定比SYN好,如果高并发执行时间久 ,用SYN好, 因为SYN底层用了wait() 阻塞后是不消耗CPU资源的。如果锁竞争不激烈说明自旋不严重,此时用CAS。

3、AQS重要方法

模版方法分为独占式跟共享式,子类根据需要不同调用不同的模版方法(讲解有点多,想看底层可直接下滑到第四章节)。

3.1 模板方法

3.1.1 独占式获取

3.1.1.1 accquire

不可中断获取锁accquire是获取独占锁方法,acquire尝试获取资源,成功则直接返回,不成功则进入等待队列,这个过程不会被线程中断,被外部中断也不响应,获取资源后才再进行自我中断selfInterrupt()。

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
  1. acquire(arg) tryAcquire(arg) 顾名思义,它就是尝试获取锁,需要我们自己实现具体细节,一般要求是:

如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。 如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。 如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态,此时锁保持计数被设置为 1。

  1. addWaiter(Node.EXCLUSIVE)

主要功能是 一旦尝试获取锁未成功,就要使用该方法将其加入同步队列尾部,由于可能有多个线程并发加入队尾产生竞争,因此采用compareAndSetTail锁方法来保证同步

  1. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

一旦加入同步队列,就需要使用该方法,自旋阻塞 唤醒来不断的尝试获取锁,直到被中断或获取到锁。

3.1.1.2 acquireInterruptibly

可中断获取锁acquireInterruptibly相比于acquire支持响应中断。

1、如果当前线程未被中断,则尝试获取锁。 2、如果锁空闲则获锁并立即返回,state = 1。 3、如果当前线程已持此锁,state + 1,并且该方法立即返回。 4、如果锁被另一个线程保持,出于线程调度目的,禁用当前线程,线程休眠ing,除非锁由当前线程获得或者当前线程被中断了,中断后会抛出InterruptedException,并且清除当前线程的已中断状态。 5、此方法是一个显式中断点,所以要优先考虑响应中断。

if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); // acquireInterruptibly 选择 interrupted = true; // acquire 的选择

3.1.1.3 tryAcquireNanos

该方法可以被中断,增加了超时则失败的功能。可以说该方法的实现与上述两方法没有任何区别。时间功能上就是用的标准超时功能,如果剩余时间小于0那么acquire失败,如果该时间大于一次自旋锁时间(spinForTimeoutThreshold = 1000),并且可以被阻塞,那么调用LockSupport.parkNanos方法阻塞线程。

doAcquireNanos内部:

nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException();

该方法一般会有以下几种情况产生:

在指定时间内,线程获取到锁,返回true。 当前线程在超时时间内被中断,抛中断异常后,线程退出。 到截止时间后线程仍未获取到锁,此时线程获得锁失败,不再等待直接返回false。

3.1.2 共享式获取

3.1.2.1 acquireShared

public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }

该模版方法的工作:

  1. 调用tryAcquireShared(arg) 尝试获得资源,返回值代表如下含义:

负数表示失败。 0 表示成功,但没有剩余可用资源。 正数表示成功,且有剩余资源。

doAcquireShared作用:

创建节点然后加入到队列中去,这一块和独占模式下的 addWaiter 代码差不多,不同的是结点的模式是SHARED,在独占模式 EXCLUSIVE。

3.1.2.2 acquireSharedInterruptibly

无非就是可中断性的共享方法

public final void acquireSharedInterruptibly(long arg) throws InterruptedException { if (Thread.interrupted()) // 如果线程被中断,则抛出异常 throw new InterruptedException(); if (tryAcquireShared(arg) < 0) // 如果tryAcquireShared()方法获取失败,则调用如下的方法 doAcquireSharedInterruptibly(arg); }

3.1.2.3. tryAcquireSharedNanos

尝试以共享模式获取,如果被中断则中止,如果超过给定超时期则失败。实现此方法首先要检查中断状态,然后至少调用一次 tryacquireshared(long),并在成功时返回。否则,在成功、线程中断或超过超时期之前,线程将加入队列,可能反复处于阻塞或未阻塞状态,并一直调用 tryacquireshared(long)。

public final boolean tryAcquireSharedNanos(long arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout); }

3.1.3 独占式释放

独占锁的释放调用unlock方法,而该方法实际调用了AQS的release方法,这段代码逻辑比较简单,如果同步状态释放成功(tryRelease返回true)则会执行if块中的代码,当head指向的头结点不为null,并且该节点的状态值不为0的话才会执行unparkSuccessor()方法。

public final boolean release(long arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

3.1.4 共享式释放

releaseShared首先去尝试释放资源tryReleaseShared(arg),如果释放成功了,就代表有资源空闲出来,那么就用doReleaseShared()去唤醒后续结点。

public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }

比如CountDownLatch的countDown()具体实现:

public void countDown() { sync.releaseShared(1); }

3.2 子类需实现方法

子类要实现父类方法也分为独占式跟共享式。

3.2.1 独占式获取

tryAcquire 顾名思义,就是尝试获取锁,AQS在这里没有对其进行功能的实现,只有一个抛出异常的语句,我们需要自己对其进行实现,可以对其重写实现公平锁、不公平锁、可重入锁、不可重入锁

protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }

3.2.2 独占式释放

tryRelease 尝试释放 独占锁,需要子类实现。

protected boolean tryRelease(long arg) { throw new UnsupportedOperationException(); }

3.2.3 共享式获取

tryAcquireShared 尝试进行共享锁的获得,需要子类实现。

protected long tryAcquireShared(long arg) { throw new UnsupportedOperationException(); }

3.2.4 共享式释放

tryReleaseShared尝试进行共享锁的释放,需要子类实现。

protected boolean tryReleaseShared(long arg) { throw new UnsupportedOperationException(); }

3.3 状态标志位

state因为用 volatile修饰 保证了我们操作的可见性,所以任何线程通过getState()获得状态都是可以得到最新值,但是setState()无法保证原子性,因此AQS给我们提供了compareAndSetState方法利用底层UnSafe的CAS功能来实现原子性。

private volatile long state; protected final long getState() { return state; } protected final void setState(long newState) { state = newState; } protected final boolean compareAndSetState(long expect, long update) { return unsafe.compareAndSwapLong(this, stateOffset, expect, update); }

3.4 查询是否独占模式

isHeldExclusively 该函数的功能是查询当前的工作模式是否是独占模式。需要子类实现。

protected boolean isHeldExclusively() { throw new UnsupportedOperationException(); }

3.5 自定义实现锁

这里需要重点说明一点,JUC中一般是用一个子类继承自Lock,然后在子类中定义一个内部类来实现AQS的继承跟使用。

public class SowhatLock implements Lock { private Sync sync = new Sync(); @Override public void lock() { sync.acquire(1); } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1,unit.toNanos(time)); } @Override public void unlock() { sync.release(1); } @Override public Condition newCondition() { return sync.newCondition(); } @Override public void lockInterruptibly() throws InterruptedException { } private class Sync extends AbstractQueuedSynchronizer { @Override protected boolean tryAcquire(int arg) { assert arg == 1; if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } @Override protected boolean tryRelease(int arg) { assert arg == 1; if (!isHeldExclusively()) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; } @Override protected boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); } Condition newCondition() { return new ConditionObject(); } } }

自定义实现类:

public class SoWhatTest { public static int m = 0; public static CountDownLatch latch = new CountDownLatch(50); public static Lock lock = new SowhatLock(); public static void main(String[] args) throws Exception { Thread[] threads = new Thread[50]; for (int i = 0; i < threads.length ; i++) { threads[i] = new Thread(()->{ try{ lock.lock(); for (int j = 0; j <100 ; j++) { m++; } }finally { lock.unlock(); } latch.countDown(); }); } for(Thread t : threads) t.start(); latch.await(); System.out.println(m); } }

4、AQS底层

4.1 CLH

CLH(Craig、 Landin、 Hagersten locks三个人名字综合而命名):

是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。 CLH 锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

4.2 Node

CLH 队列由Node对象组成,其中Node是AQS中的内部类。

static final class Node { // 标识共享锁 static final Node SHARED = new Node(); // 标识独占锁 static final Node EXCLUSIVE = null; // 前驱节点 volatile Node prev; // 后继节点 volatile Node next; // 获取锁失败的线程保存在Node节点中。 volatile Thread thread; // 当我们调用了Condition后他也有一个等待队列 Node nextWaiter; //在Node节点中一般通过waitStatus获得下面节点不同的状态,状态对应下方。 volatile int waitStatus; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3;

waitStatus 有如下5中状态:

  1. CANCELLED = 1

表示当前结点已取消调度。当超时或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

  1. SIGNAL = -1

表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为 SIGNAL。

  1. CONDITION = -2

表示结点等待在 Condition 上,当其他线程调用了 Condition 的 signal() 方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  1. PROPAGATE = -3

共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  1. INITIAL = 0

新结点入队时的默认状态。

4.3 AQS实现

4.3.1 公平锁和非公平锁

银行售票窗口营业中:

公平排队:每个客户来了自动在最后面排队,轮到自己办理业务的时候拿出身份证等证件取票。 非公平排队:有个旅客火车马上开车了,他拿着自己的各种证件着急这想跟窗口工作人员说是否可以加急办理下,可以的话则直接办理,不可以的话则去队尾排队去。

在JUC中同样存在公平锁跟非公平锁,一般非公平锁效率好一些。因为非公平锁状态下打算抢锁的线程不用排队挂起了。

4.3.2 AQS细节

AQS内部维护着一个FIFO的队列,即CLH队列,提供先来先服务的公平性。AQS的同步机制就是依靠CLH队列实现的。CLH队列是FIFO的双端双向链表队列(方便尾部节点插入)。线程通过AQS获取锁失败,就会将线程封装成一个Node节点,通过CAS原子操作插入队列尾。当有线程释放锁时,会尝试让队头的next节点占用锁,个人理解AQS具有如下几个特点:

在AQS 同步队列中 -1 表示线程在睡眠状态 当前Node节点线程会把前一个Node.ws = -1。当前节点把前面节点ws设置为-1,你可以理解为:你自己能知道自己睡着了吗?只能是别人看到了发现你睡眠了! 持有锁的线程永远不在队列中。 在AQS队列中第二个才是最先排队的线程。 如果是交替型任务或者单线程任务,即使用了Lock也不会涉及到AQS 队列。 不到万不得已不要轻易park线程,很耗时的!所以排队的头线程会自旋的尝试几个获取锁。

4.4 加锁跟解锁流程图

以最经典的 ReentrantLock 为例逐步分析下 lock 跟 unlock 底层流程图(要原图的话公众号回复:lock)。

private Lock lock = new ReentrantLock(); public void test(){ lock.lock(); try{ doSomeThing(); }catch (Exception e){ ... }finally { lock.unlock(); } }

4.4.1 独占式加入同步队列

同步器AQS中包含两个节点类型的引用:一个指向头结点的引用(head),一个指向尾节点的引用(tail),如果加入的节点是OK的则会直接运行该节点,当若干个线程抢锁失败了那么就会抢着加入到同步队列的尾部,因为是抢着加入这个时候用CAS来设置尾部节点。入口代码:

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
  1. tryAcquire

该方法是需要自我实现的,在上面的demo中可见一斑,就是返回是否获得了锁。

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { 
// 是否需要加入队列,不需要的话则尝试CAS获得锁,获得成功后 设置当前锁的拥有者 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { 
// 这就是可重入锁的实现 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
  1. addWaiter(Node.EXCLUSIVE,arg)

/** * 如果尝试获取同步状态失败的话,则构造同步节点(独占式的Node.EXCLUSIVE),通过addWaiter(Node node,int args)方法将该节点加入到同步队列的队尾。 */ private Node addWaiter(Node mode) { 
// 用当前线程构造一个Node对象,mode是一个表示Node类型的字段,或者说是这个节点是独占的还是共享的 Node node = new Node(Thread.currentThread(), mode); 
// 将目前队列中尾部节点给pred Node pred = tail; 
// 队列不为空的时候 if (pred != null) { node.prev = pred; 
// 先尝试通过AQS方式修改尾节点为最新的节点,如果修改失败,意味着有并发, if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } 
//第一次尝试添加尾部失败说明有并发,此时进入自旋 enq(node); return node; }
  1. 自旋enq 方法将并发添加节点的请求通过CAS跟自旋将尾节点的添加变得串行化起来。说白了就是让节点放到正确的队尾位置。

/** * 这里进行了循环,如果此时存在了tail就执行同上一步骤的添加队尾操作,如果依然不存在, * 就把当前线程作为head结点。插入节点后,调用acquireQueued()进行阻塞 */ 
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { 
// Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
  1. acquireQueued 是当前Node节点线程在死循环中获取同步状态,而只有前驱节点是头节点才能尝试获取锁,原因是:

头结点是成功获取同步状态(锁)的节点,而头节点的线程释放了同步状态以后,将会唤醒其后继节点,后继节点的线程被唤醒后要检查自己的前驱节点是否为头结点。 维护同步队列的FIFO原则,节点进入同步队列之后,会尝试自旋几次。

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)) { 
// 节点中的线程循环的检查,自己的前驱节点是否为头节点 
// 只有当前节点 前驱节点是头节点才会 再次调用我们实现的方法tryAcquire 
// 接下来无非就是将当前节点设置为头结点,移除之前的头节点 setHead(node); p.next = null; // help GC failed = false; return interrupted; } 
// 否则检查前一个节点的状态,看当前获取锁失败的线程是否要挂起 if (shouldParkAfterFailedAcquire(p, node) && 
//如果需要挂起,借助JUC包下面的LockSupport类的静态方法park挂起当前线程,直到被唤醒 parkAndCheckInterrupt()) interrupted = true; 
// 两个判断都是true说明 则置true } } finally { 
//如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。 if (failed) 
//取消请求,将当前节点从队列中移除 cancelAcquire(node); } }

如果成功就返回,否则就执行shouldParkAfterFailedAcquire、parkAndCheckInterrupt来达到阻塞效果。

  1. shouldParkAfterFailedAcquire 第二步的addWaiter()构造的新节点,waitStatus的默认值是0。此时,会进入最后一个if判断,CAS设置pred.waitStatus SIGNAL,最后返回false。由于返回false,第四步的acquireQueued会继续进行循环。假设node的前继节点pred仍然不是头结点或锁获取失败,则会再次进入shouldParkAfterFailedAcquire()。上一轮循环中已经将pred.waitStatu = -1了,则这次会进入第一个判断条件,直接返回true,表示应该阻塞调用parkAndCheckInterrupt。

那么什么时候会遇到ws > 0呢?当pred所维护的获取请求被取消时(也就是node.waitStatus = CANCELLED,这时就会循环移除所有被取消的前继节点pred,直到找到未被取消的pred。移除所有被取消的前继节点后,直接返回false。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 获得前驱节点的状态 if (ws == Node.SIGNAL) //此处是第二次设置 return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 此处是第一次设置 unsafe级别调用设置 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
  1. parkAndCheckInterrupt 主要任务是暂停当前线程然后查看是否已经暂停了。

private final boolean parkAndCheckInterrupt() { // 调用park()使线程进入挂起状态,什么时候调用了unpark再继续执行下面 LockSupport.park(this); // 如果被唤醒,查看自己是不是已经被中断了。 return Thread.interrupted(); }
  1. cancelAcquireacquireQueued方法的finally会判断 failed值,正常运行时候自旋出来的时候会是false,如果中断或者timeout了 则会是true,执行cancelAcquire,其中核心代码是node.waitStatus = Node.CANCELLED。

  2. selfInterrupt

static void selfInterrupt() { Thread.currentThread().interrupt(); }

4.4.2 独占式释放队列头节点

release()会调用tryRelease方法尝试释放当前线程持有的锁,成功的话唤醒后继线程,并返回true,否则直接返回false。

public final boolean release(long arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
  1. tryRelease 这个是子类需要自我实现的,没啥说的根据业务需要实现。

  2. unparkSuccessor 唤醒头结点的后继节点。

private void unparkSuccessor(Node node) { int ws = node.waitStatus; // 获得头节点状态 if (ws < 0) //如果头节点装小于0 则将其置为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) //从队列尾部开始往前去找最前面的一个waitStatus小于0的节点 if (t.waitStatus <= 0) s = t; } if (s != null)//唤醒后继节点对应的线程 LockSupport.unpark(s.thread); }

4.4.3 AQS 中增加跟删除形象图

5、CountDownLatch底层

5.1 共享锁 CountDownLatch底层

CountDownLatch 虽然相对简单,但也实现了共享锁模型。但是如何正确的吹逼 CountDownLatch 呢?如果在理解了上述流程的基础上,从CountDownLatch入手来看 AQS 中关于共享锁的代码还比较好看懂,在看的时候可以 以看懂大致内容为主,学习其设计的思路,不要陷入所有条件处理细节中,多线程环境中,对与错有时候不是那么容易看出来的。个人追源码绘制了如下图:

5.2 计数信号量Semaphore

Semaphore 这就是共享锁的一个实现类,在初始化的时候就规定了共享锁池的大小N,有一个线程获得了锁,可用数就减少1个。有一个线程释放锁可用数就增加1个。如果有 >=2 的线程同时释放锁,则此时有多个锁可用。这个时候就可以 同时唤醒 两个锁 setHeadAndPropagate (流程图懒的绘制了)。

public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { //找先驱结点 final Node p = node.predecessor(); if (p == head) { // 尝试获取资源 int r = tryAcquireShared(arg); if (r >= 0) { // 设置当前结点为头结点,然后去唤醒后续结点。注意传播性 唤醒! setHeadAndPropagate(node, r); p.next = null; // help GC 释放头结点,等待GC if (interrupted) selfInterrupt(); failed = false;//获取到资源 return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed)//如果最后没有获取到资源,则cancel cancelAcquire(node); } }

5.3 ReentrantReadWriteLock

​在 ReentrantReadWriteLock 类中也是只有一个32位的int state来表示读锁跟写锁,如何实现的?

后16位用来保存独享的写锁个数,第一次获得就是01,第二次重入就是10了,这样的方式来保存。 但是多个线程都可以获得读锁,并且每个线程可能读多次,如何保存?我们用前16位来保存有多少个线程获得了读锁。 每个读锁线程获得的重入读锁个数 由内部类HoldCounter与读锁配套使用。

6、Condition

synchronized 可用 wait() 和 notify()/notifyAll() 方法相结合可以实现等待/通知模式。Lock 也提供了 Condition 来提供类似的功能。

Condition是JDK5后引入的Interface,它用来替代传统的Object的wait()/notify()实现线程间的协作,相比使用Object的wait()/notify(),使用Condition的await()/signal()这种方式 实现线程间协作更加安全和高效。简单说,他的作用是使得某些线程一起等待某个条件(Condition),只有当该条件具备(signal 或者 signalAll方法被调用)时,这些等待线程才会被唤醒,从而重新争夺锁。wait()/notify()这些都更倾向于底层的实现开发,而Condition接口更倾向于代码实现的等待通知效果。两者之间的区别与共通点如下:

6.1 条件等待队列

条件等待队列,指的是 Condition 内部自己维护的一个队列,不同于 AQS 的 同步等待队列。它具有以下特点:

要加入条件等待队列的节点,不能在 同步等待队列。 从 条件等待队列 移除的节点,会进入同步等待队列。 一个锁对象只能有一个同步等待队列,但可以有多个条件等待队列。

这里以 AbstractQueuedSynchronizer 的内部类 ConditionObject 为例(Condition 的实现类)来分析下它的具体实现过程。首先来看该类内部定义的几个成员变量:

/** First node of condition queue. */ private transient Node firstWaiter; /** Last node of condition queue. */ private transient Node lastWaiter;

它采用了 AQS 的 Node 节点构造(前面说过Node类有nextWaiter属性),并定义了两个成员变量:firstWaiter、lastWaiter 。说明在 ConditionObject 内部也维护着一个自己的单向等待队列。目前可知它的结构如下:

6.2 await、signal

比如有线程 1、2竞争锁,下面来说下具体过程 线程1:

1、线程1 调用 reentrantLock.lock时,持有锁。 2、线程1 调用 await 方法,进入条件等待队列 ,同时释放锁。 3、线程1 获取到线程2 signal 信号,从条件等待队列进入同步等待队列。

线程2:

1、线程2 调用 reentrantLock.lock时,由于锁被线程1 持有,进入同步等待队列 。 2、由于线程1 释放锁,线程2 从同步等待队列 移除,获取到锁。 3、线程2 调用 signal 方法,导致线程 1 被唤醒。线程2 调用unlock ,线程1 获取锁后继续下走。

6.2.1 await

当我们看await、signal 的源码时候不要认为等待队列跟同步队列是完全分开的,其实个人感觉底层源码是有点 HashMap 中的红黑树跟双向链表的意思。

当调用await方法时候,说明当前任务队列的头节点拿着锁呢,此时要把该Thread从任务队列挪到等待队列再唤醒任务队列最前面排队的运行任务,如图:

  1. thread 表示节点存放的线程。

  2. waitStatus 表示节点等待状态。条件等待队列中的节点等待状态都是 CONDITION,否则会被清除。

  3. nextWaiter 表示后指针。

6.2.2 signal

当我们调用signal方法的时候,我们要将等待队列中的头节点移出来,让其去抢锁,如果是公平模式就要去排队了,流程如图:

上面只是形象流程图,如果从代码级别看的话大致流程如下:

6.2.3 signalAll

signalAll 与 signal 方法的区别体现在 doSignalAll 方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,doSignalAll方法只不过将等待队列中的每一个节点都移入到同步队列中,即通知当前调用condition.await()方法的每一个线程:

private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; } while (first != null); // 循环 }

6.3 End

一个 Condition 对象就有一个单项的等待任务队列。在一个多线程任务中我们可以new出多个等待任务队列。比如我们new出来两个等待队列。

private Lock lock = new ReentrantLock(); private Condition FirstCond = lock.newCondition(); private Condition SecondCond = lock.newCondition();

所以真正的AQS任务中一般是一个任务队列N个等待队列的,因此我们尽量调用signal而少用signalAll,因为在指定的实例化等待队列中只有一个可以拿到锁的。

Synchronized 中的 wait 跟 notify 底层代码的等待队列只有一个,多个线程调用wait的时候我们是无法知道头节点是那个具体线程的。因此只能notifyAll。

7、参考

1、详解Condition的await和signal:https://www.jianshu.com/p/28387056eeb4 2、Condition的await和signal流程:https://www.cnblogs.com/insaneXs/p/12219097.html

Java知识点详细解读,AQS详解 将复杂的同步问题抽象为简单的模板方法,使得开发者可以专注于资源的获取与释放逻辑,而无需处理线程阻塞、唤醒等底层细节。它是Java并发编程的基石,深刻影响了。假设有一个数据库连接池,需要支持多个线程同时访问,但每个时刻只能有一个线程获取连接。,它通过AQS的框架支持可重入、公平锁、超时机制等高级特性,完美解决上述问题。包的设计,为构建高性能、可扩展的并发工具提供了统一的框架。 阅读详情

相关推荐

深入了解 AQS

统一同步控制的底层结构,简化开发复杂同步器。支持中断、超时、公平性等高级功能。封装了线程阻塞与唤醒的复杂逻辑。AQS(AbstractQueuedSynchronizer)通过 state变量 + CLH 队列管理线程同步,利用CAS + 自旋 + 阻塞队列实现高效的独占锁和共享锁机制,是 JUC 并发工具的核心。AQS 是并发框架中锁与同步器的核心抽象类。它通过 state 状态值 和 CLH 队列来管理资源获取与线程排队。支持独占模式与共享模式,满足不同同步需求。

m0_73748193的博客 884

AQS简单介绍与使用

AQS,全称是 AbstractQueuedSynchronizer,中文译为抽象队列式同步器。这个抽象类对于JUC并发包非常重要,JUC包中的ReentrantLock,,Semaphore,ReentrantReadWriteLock,CountDownLatch 等等几乎所有的类都是基于AQS实现的。 AQS 中有两个重要的东西,一个等待队列,还有一个STATE标志,并且通过CAS来改变它的...

charming的专栏 2万+

(面经总结)一篇文章带你完整复习 Java 中的 AQS

文章目录一、什么是AQS二、AQS的原理三、state:状态四、AQS共享资源的方式:独占式和共享式 一、什么是AQS AQS(Abstract Queued Synchronizer)是一个抽象的队列同步器,通过维护一个共享资源状态(Volatile Int State)和一个先进先出(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。 二、AQS的原理 AQS为每个共享资源都设置一个共享资源锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该

南淮北安的博客 2021

吐血总结:AQS到底是什么?

文章目录1、概述2、基本框架2.1、AQS框架介绍2.2、AQS核心成员变量和方法3、源码分析3.1、CLH队列(FIFO)3.2、独占模式获取资源3.2.1、acquire(int)3.2.1.1、tryAcquire(int arg)3.2.1.2、addWaiter(Node mode)3.2.1.3、acquireQueued(final Node node, int arg)3.2.1.3.1、shouldParkAfterFailedAcquire(p, node)3.2.1.3.2、parkA

u011047968的专栏 7051

图像相似度的评价指标 : FID(Fréchet Inception Distance)

FID(Fréchet Inception Distance) 是用来计算真实图像与生成图像的特征向量间距离的一种度量。如果FID值越小,则相似程度越高。最好情况即是FID=0,两个图像相同。 实际计算: 参考链接:https://machinelearningmastery.com/how-to-implement-the-frechet-inception-distance-fid-from-scratch/ # example of calculating the frechet inceptio

weixin_43723625的博客 1万+

动态欧拉角与静态欧拉角:从理论到实践的深度解析

本文深度解析了动态欧拉角与静态欧拉角的核心区别与应用场景。静态欧拉角基于固定世界坐标系,适用于全局导航与三维场景编辑;而动态欧拉角基于物体自身坐标系,更符合飞行模拟、机器人控制等直觉操作,但需警惕其固有的万向节死锁问题。文章通过理论结合实战,帮助开发者在机器人、无人机及图形学项目中正确选用与处理旋转表示。

java5的博客 1074

AQS 工作原理

而且tryRelease()的返回值代表着该线程是否已经完成资源的释放,因此在自定义同步器的tryRelease()时,需要明确这条件,当已经彻底释放资源(state=0),要返回true,否则返回false。unparkSuccessor用unpark()唤醒等待队列中最前驱的那个未放弃线程,此线程并不一定是当前节点的next节点,而是下一个可以用来唤醒的线程,如果这个节点存在,调用unpark()方法唤醒。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。

worn_xiao的博客 9001

如何在Jetson Nano上快速搭建Pytorch环境(含torchvision安装教程)

本文详细介绍了在Jetson Nano上快速搭建PyTorch环境的完整步骤,包括系统准备、PyTorch安装优化、torchvision编译安装及环境测试。通过本指南,开发者可以高效部署深度学习开发环境,充分利用Jetson Nano的GPU加速能力进行计算机视觉项目开发。

weixin_42628941的博客 341

谈谈你对AQS的了解

前言:今天无意中看到了"中华石衫"大佬的公众号的文章,觉得很不错,就摘录下来了。 1.写在前面: 这篇文章,我们来聊聊面试时一个比较有杀伤力的问题:聊聊你对AQS的理解? 之前有同学反馈,去互联网公司面试,面试官聊到并发时就问到了这个问题。当时那位同学内心估计受到了一万点伤害。。。因为首先,很多人可能连AQS是什么都不知道。或者仅仅是听说过AQS这个名词,但是可能连全称怎么拼写都不知道。更有...

oldshaui的博客 7307

用运放和乘法器DIY忆阻模拟器:从理论到Multisim仿真实战

本文详细介绍了如何利用运放和乘法器DIY忆阻模拟器,从理论基础到Multisim仿真实战。通过解析忆阻器的数学模型,设计关键电路模块,并完成仿真验证,帮助电子爱好者低成本实现忆阻器功能。文章还提供了实际调试中的常见问题解决方案和进阶应用建议,适合神经形态计算和非易失性存储领域的研究者参考。

weixin_28718345的博客 421

Livox MID360与Fast-LIO2的实战部署指南:从驱动安装到点云处理

本文提供Livox MID360激光雷达与Fast-LIO2激光惯性里程计从零开始的实战部署指南。详细介绍了Ubuntu系统下的驱动安装、网络配置、点云数据验证以及Fast-LIO2算法的编译、参数配置与联合启动流程,帮助开发者快速搭建移动机器人SLAM系统,并包含关键参数调优与常见故障排查方法。

java5的博客 1026

AQS源码详细解读

引言 AQS是用来构建锁和其他同步组件的基础框架,它也是Java三大并发工具类(CountDownLatch、CyclicBarrier、Semaphore)的基础。ReentrantLock,甚至BlockingQueue也是基于它的实现,可以说是非常重要了。 简单介绍一下,AQS其实就是一个类,全称是AbstractQueuedSynchronizer,队列同步器。本文的重点是研究它的源码...

luluyo的博客 1096

Nmap源码分析(端口扫描)

端口扫描是Nmap的核心功能,用于确定目标机的端口状态(开放、关闭、过滤等),也为Nmap的服务与版本扫描、OS扫描、脚本扫描提供基本的指引信息。所以,深入理解端口扫描的实现对分析其他的扫描方式很有帮助。 1简单回顾   首先简要回顾一下端口扫描的原理以及命令行选项。 1.1扫描原理   Nmap提供的10多种类型的端口扫描方法,如TCP SYN/ACK/FIN/Xmas/NULL/ Windows/Connect,FTP Bounce, Idle scan, UDP port u...

墨痕诉清风的博客 1599

多线程/并发编程——两万字详解AQS

多线程/并发编程——并发锁的底层实现AQS详解 文章目录多线程/并发编程——并发锁的底层实现AQS详解一、AQS概要1、AQS 概念2、AQS 的同步队列模型二、状态位 state三、Node 节点四、出入同步队列的实现--CAS五、基于CountDownLatch分析AQS共享模式的实现六、基于ReentrantLock分析AQS独占模式的实现1、ReentrantLock 与 AQS 的关系2、ReentrantLock非公平锁的实现3、ReentrantLock非公平锁的实现 在前面我们学习 Reen

qq_42583242的博客 713

ARM Linux工控板+C# .NET Core:倍福ADS协议轻量化工业控制(附跨平台部署代码)

某自动化设备厂的倍福PLC控制系统曾依赖传统x86工控机:单台设备需配备1台Intel i5工控机(成本约8000元),功耗30W+,且Windows系统稳定性受工业环境干扰(如电磁辐射导致蓝屏)。更棘手的是,设备需部署在产线边缘(空间狭小),x86工控机的体积(4U机架)和散热需求(风扇噪音)难以适配;而多设备部署时,高硬件成本和高功耗导致年运维成本超20万元。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 954

支持向量机算法之鸢尾花特征分类【机器学习】

支持向量机(SVM)是一种二元分类模型。它的基本模型是在特征空间中定义最大区间的线性分类器,这使它不同于感知器;支持向量机还包括核技术,这使得它本质上是一个非线性分类器。支持向量机的学习策略是区间最大化,它可以形式化为求解凸二次规划的问题,等价于正则化铰链损失函数的最小化。支持向量机的学习算法是求解凸二次规划的优化算法。Scikit learn(sklearn)是机器学习中常见的第三方模块。它封装了常见的机器学习方法,包括回归、降维、分类、聚类等。实例的特征向量(以2D为例)映射到空间中的一些点,如下图中的

一键难忘的博客 4971

AQS你真的了解吗?

本文由浅入深对AQS进行了解读,其中主要包括其数据结构、等待队列和条件队列等。然而虽然AQS从原理上看是十分精细并复杂,但是作为同步/阻塞的框架使用起来所需要做的事情其实并不多,下面笔者将从使用者的角度列举出AQS的用法。...

m0_48922996的博客 853

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

【JDK源码】AQS源码详细解读

AQS源码详细解读 什么是AQAS AQS:AbstractQueuedSynchronizer ,它是一个抽象的同步队列器,这个类位于java.util.concurrent.locks包下面,是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量麦示持有锁的状态。 主要是通过CLH队列实现对线程的调度。 CLH: Craig、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CL

flw7758的博客 508

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

JAVA面试题|AQS详细剖析

JAVA基础篇面试题 文章目录JAVA基础篇面试题1. 线程等待唤醒的实现方法2. 介绍一下LockSupport3. AQS是什么4. AQS的核心原理5. 公平锁与非公平锁的区别6. 请讲述非公平锁加锁解锁的具体流程 1. 线程等待唤醒的实现方法 Object对象中的wait()方法可以让线程等待,使用Object中的notify()方法唤醒线程; 必须都在同步代码块内使用; 调用wait,notify的对象是加锁对象; notify必须在wait后执行才能唤醒; wait后能释放锁对象,线程处

qq_37419449的博客 1万+

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

AQS解读及AQS相关实现类

AQS解读及相关AQS实现类 1. AQS概述 简述:AQS全称AbstractQueuedSynchronizer,即抽象队列同步器。AQS是用来构建锁或者其他同步组件的基础框架,它使用一个整型的volatile变量state来维护同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。AQS为一系列同步器依赖于一个单独的原子变量state的同步器提供了一个非常有用的基础。 AQS的设计是...

java_lg的博客 2129

爆肝万字,AQS 源码分析!

最近一周在写 AQS 源码分析,AQS 属于并发的核心内容,很多同步器都基于 AQS 实现。可能很多同学并不太了解 AQS 学习到什么程度好,以及 AQS 中的 Node 节点状态是在是太复杂,看的眼花缭乱!通过这一周看 AQS 源码的经验,我觉得学习 AQS 只要把它内部的执行流程捋顺就可以了。AQS 主要是管理线程的入队、出队,以及队列中线程等待获取资源这一过程,所以自己在学习的过程中也有必要画画图,来看一下在入队、出队的时候,节点的状态是如何变化。通过画图,可以很好地理清思路。

qq_45260619的博客 1327

面试题:了解Java的AQS吗?

前言java.util.concurrent包(之后简称JUC包)中,提供了大量的同步与并发的工具类,是多线程编程的“利器”。其中locks包下,包含了多种锁,如Reen...

IT技术精选文摘 1万+

JUC并发编程基石——AQS深入解读

Java.util.concurrent 是在并发编程中比较常用的工具类,里面包含很多用来在并发 场景中使用的组件。比如ReentrantLock,Semaphore,CountDownLatch、ThreadPoolExecutor等等。并发包的作者是大名鼎鼎的 Doug Lea。 文章目录深入解读AQS源码前言什么是CASLockSupport工具类独占模式获取锁释放锁共享模式获取共享锁释放共享锁线程间通信等待通知

鲨鱼大叔的博客 785

JUC之AQS解读

JUC之AQS解读 文章目录JUC之AQS解读一、队列同步器二、设计思想1、同步队列节点2、同步状态处理(独占)3、同步状态处理(共享)questionanswer 一、队列同步器 ​ AbstractQueueSynchronizer,即队列同步器,通常被称为AQS,是用于构造锁、同步组件的基础框架,它使用int表示同步状态,使用内置FIFO双向队列来完成线程的排队工作。 ​ AQS可以说是整个...

rekingman的博客 751
上一篇: 带你了解什么是Java虚拟机运行时数据区
下一篇: 在哪些场景下不建议自增数据库主键
Java_LingFeng
博客等级 码龄4年 505粉丝 259原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值