1.JUC 并发工具-AQS(AbstractQueuedSynchronizer)

1.AQS概述

AQS(AbstractQueuedSynchronizer)是JUC并发包中的一个基类,用于构建锁和其他同步组件,如 ReentrantLock、Semaphore、CyclicBarrier、CountDownLatch、ThreadPoolExecutor等等都是基于AQS实现。
接下来本篇文章将以 ReentrantLock 做分析AQS的加锁、释放锁、取消锁来的过程。

2.为什么需要AQS

传统同步机制 synchronized 使用简单直接,其底层原理是编译器编译后会在同步块前后生成 monitorenter 和 monitorexit 字节码指令,靠对象头里的标记来控制锁的获取和释放。这样的同步机制功能单一,拓展性差,不够灵活。

AQS 则解决了同步器设计的通用性问题,AQS 将同步状态、线程排队、等待/唤醒等通用逻辑抽象出来,开发者能够根据业务情况自定义或拓展所需的同步规则,实现更精细化的并发控制。

3.核心属性

在AQS中,其内部关键要素主要有以两大核心:

  • state:一个由 volatile 修饰,并且基于CAS修改的 int 类型的成员变量,
  • node双向队列:通过Node对象串联起来的双向链表,当线程竞争state 资源失败时,会把自己封装为 Node 对象放到双向队列中排队。Node 是AQS中的内部类,其中主要的成员变量如下:
    static final class Node {
    	// 共享锁,表示当前创建的节点是共享锁
        static final Node SHARED = new Node();
        // 排他锁,表示当前创建的节点是排他锁
        static final Node EXCLUSIVE = null;
        // waitStatus的值,表示当前节点取消状态
        static final int CANCELLED =  1;
        // 表示队列后续节点待唤醒
        static final int SIGNAL    = -1;
        // waitStatus的值,表示该结点(对应的线程)在等待某一条件
        static final int CONDITION = -2;
        // waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)
        static final int PROPAGATE = -3;
        // 描述当前节点状态,取值:-1,-2,-3,0,默认值为 0
        volatile int waitStatus;
        // 当前节点的前一个节点
        volatile Node prev;
        // 当前节点的后继节点
        volatile Node next;
        // 当前节点对应的线程
        volatile Thread thread; 
    }
    

4.从源码看AQS的工作原理

下面将从 ReentrantLock 入手分析AQS加锁和释放锁的流程

4.1.加锁流程分析

以公平所为例

4.1.1.概述

在这里插入图片描述

如上图,假设有A、B、C、D 4个线程竞争锁;

  1. 线程A先基于CAS将 state 从0 修改为 1,线程A就获取到锁资源,可以执行业务代码
  2. 线程B、C、D再想通过CAS修改 state 状态,发现已经是 1,无法获取锁资源
  3. 线程B、C、D依次将自己封装为Node对象,加入到双向队列中排队。
    3.1. 如果队列为空,则新建一个伪节点作为头节点(伪节点 thread = null)加入到双向队列中
    3.2. 如果队列不为空,将B、C、D线程对应的Node挂到tail节点后面,修改上一个节点waitStatus=-1,把 tail 指向队列最后一个node节点

4.1.2. lock 源码分析

4.1.2.1. lock方法
  1. 执行lock方法后,公平锁和非公平锁执行逻辑有所不同
// 非公平锁
final void lock() {
	// 上来就基于CAS的方式,尝试将state从0改为1
    if (compareAndSetState(0, 1))
    	// 获取锁成功,设置 exclusiveOwnerThread 为当前线程,表示当前线程持有锁资源,后续判断锁重入也用到
		setExclusiveOwnerThread(Thread.currentThread());
    else
    	// 上锁失败
        acquire(1);
}

// 公平锁
final void lock() {
    acquire(1);
}
  1. acquire 方法(AQS的方法),从上面的代码看出,不管公平锁还是非公平锁,在获取锁的过程中,都有可能执行 acquire ,非公平锁在一开始上锁失败才会执行 acquire 方法,所以 公平锁和非公平锁的 acquire 的逻辑都一样
public final void acquire(int arg) {
	// 当前线程尝试获取锁
    if (!tryAcquire(arg) &&
    	// 获取锁失败
    	// addWaiter(Node.EXCLUSIVE):把当前包装成 Node 节点,加入到双休队列尾部
    	// acquireQueued:判断新加入节点是否是第一个排队,如果是,则尝试再获取锁,获取不到则挂起线程; 如果不是第一个排队的节点,则直接挂起线程
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 中断线程
        selfInterrupt();
}
  1. 非公平锁 tryAcquire 的实现
// 非公平锁的实现
final boolean nonfairTryAcquire(int acquires) {
	// 获取当前线程
    final Thread current = Thread.currentThread();
    int c = getState();
    // 判断当前 state 是否为0
    if (c == 0) {
        // 再抢一次锁
        if (compareAndSetState(0, acquires)) {
        	// 抢锁成功,设置当前线程为获得锁资源线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // state 不为0,判断当前取得锁的线程是否等于当前线程
    else if (current == getExclusiveOwnerThread()) {
    	// 如果是,锁重入,state + 1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
        	// 锁重入不能超过 int 数值最大值。
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  1. 公平锁 tryAcquire 的实现
// 公平锁 tryAcquire 的实现
protected final boolean tryAcquire(int acquires) {
	// 获取当前线程
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	// 判断队列中是否有节点在排队,如果没有在排队的,抢一下锁资源。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 抢锁成功,设置当前线程为获取锁资源的线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
    	// 当前线程 == 获取锁资源的线程,锁重入,state + 1,
    	// state 累加到 int 值极限时,会变为负值,所以 state 的最大值就是 int 的最大值。
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

// 查看是否有线程在AQS的双向队列中排队
// 返回false,代表没人排队
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // 如果头尾节点相等,证明没有线程排队,直接去抢占锁资源
    return h != t &&
    // s节点不为null,并且s节点的线程为当前线程(排在第一名的是不是我)
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

  1. acquireQueued方法,判断当前线程是否还能再次尝试获取锁资源,如果不能再次获取锁资源,或者又没获取到,尝试将当前线程挂起
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)) {
            	// 如果上锁成功,把当前获取锁成功的node设置到头节点, 
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 没获取到锁资源
            // shouldParkAfterFailedAcquire:基于上一个节点状态来判断当前节点是否能够挂起线程,如果可以返回true,如果不能,就返回false,继续下次循环
            if (shouldParkAfterFailedAcquire(p, node) &&
            	// parkAndCheckInterrupt 基于Unsafe类的park方法,将当前线程挂起
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

// 获取锁资源成功后,先执行setHead
private void setHead(Node node) {
    // 当前节点作为头结点  伪
    head = node;
    // 头结点不需要线程信息
    node.thread = null;
    node.prev = null;
}

// 当前Node没拿到锁资源,判断是否能挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// -1,SIGNAL状态:代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点
    // 1,CANCELLED状态:代表当前节点已经取消
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
    	// 上一个节点为-1之后,当前节点才可以安心的挂起线程
        return true;
    if (ws > 0) {
        // 如果当前节点的上一个节点是取消状态,需要遍历往前找到一个状态不为1的Node,作为当前他的next节点
        // 找到状态不为1的节点后,设置一下next和prev
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
    	// 上一个节点的状态不是1或者-1,那就代表节点状态正常,将上一个节点的状态改为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
4.1.2.2. tryLock方法

对于公平锁和非公平锁,最终都会走非公平锁抢占锁资源的操作,拿到state,如果是0,尝试CAS抢一下,不是0,那就判断是否重入锁。如果还没抢到,就返回false,抢锁失败

// tryLock方法,无论公平锁还有非公平锁。都会走非公平锁抢占锁资源的操作
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	// 拿到state的值, 如果是0,直接CAS浅尝一下
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //state 不是0,那就看下是不是锁重入操作
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 如果没抢到,或者不是锁重入操作,告辞,返回false
    return false;
}

tryLock(time,unit) 分析
具体步骤分为:

  1. 判断现场是否被中断,如果中断直接抛异常;
  2. 尝试通过 tryAcquire 获取锁,如果获取锁失败,则等待指定时间
    2.1. 设置结束时间,把节点放到双向队列中
    2.2. for死循环从双向队列,向前遍历获取node 节点,如果当前节点的上一个节点为 head 节点,直接抢资源。
    3.3. 如果没有抢到锁,剩余足够的时间,挂起线程剩余时间。
public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

// tryLock(time,unit)执行的方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {
    // 线程的中断标记位,是不是从false,被改为了true,如果是,直接抛异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // tryAcquire分为公平和非公平锁两种执行方式,如果拿锁成功, 直接告辞,
    return tryAcquire(arg) ||
        // 如果拿锁失败,在这要等待指定时间
        doAcquireNanos(arg, nanosTimeout);
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 如果等待时间是0秒,直接告辞,拿锁失败  
    if (nanosTimeout <= 0L)
        return false;
    // 设置结束时间。
    final long deadline = System.nanoTime() + nanosTimeout;
    // 先扔到AQS队列
    final Node node = addWaiter(Node.EXCLUSIVE);
    // 拿锁失败,默认true
    boolean failed = true;
    try {
        for (;;) {
            // 如果在AQS中,当前node是head的next,直接抢锁
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 结算剩余的可用时间
            nanosTimeout = deadline - System.nanoTime();
            // 判断是否是否用尽的位置
            if (nanosTimeout <= 0L)
                return false;
            // shouldParkAfterFailedAcquire:根据上一个节点来确定现在是否可以挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 避免剩余时间太少,如果剩余时间少就不用挂起线程
                nanosTimeout > spinForTimeoutThreshold)
                // 如果剩余时间足够,将线程挂起剩余时间
                LockSupport.parkNanos(this, nanosTimeout);
            // 如果线程醒了,查看是中断唤醒的,还是时间到了唤醒的。
            if (Thread.interrupted())
                // 是中断唤醒的!
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
4.1.2.3. lockInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

4.2.释放锁流程分析

4.2.1.概述

在这里插入图片描述

如上图,假设线程A获取到锁资源,重入了一次,state=2
线程 B、C、D 获取锁资源失败,在AQS中排队
1、线程A释放调用 unlock 锁资源,执行 tryRelease 方法
2、判断是否是线程A持有锁资源,如果不是,抛出异常
3、如果是线程A,对 state-1,如果 state扣减1后,不为0,结束方法
4、如果state-1为0,则资源释放干净,查看头节点的状态是否不为0,如果为0,说明后面没有挂起的线程,如不为0,后续链表中有挂起的线程,需要唤醒。
5、在唤醒线程时,需要将当前的-1改为0,找到有效节点唤醒。

4.2.2.unlock源码分析

public void unlock() {
    // 释放锁资源不分为公平锁和非公平锁,都执行同一个方法
    sync.release(1);
}

// 释放锁流程
public final boolean release(int arg) {
    // 核心释放锁资源的操作之一
    if (tryRelease(arg)) {
        // 如果锁已经释放掉了,走这个逻辑
        Node h = head;
        // h不为null并且状态不为0(为-1),后面有排队的Node,线程处于挂起状态
        if (h != null && h.waitStatus != 0)
            // 唤醒排队的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// ReentrantLock 释放锁资源操作
protected final boolean tryRelease(int releases) {
    // 拿到state - 1(并没有赋值给state)
    int c = getState() - releases;
    // 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // free,代表当前锁资源是否释放干净了。
    boolean free = false;
    if (c == 0) {
        // 如果state - 1后的值为0,代表释放干净了。
        free = true;
        // 将持有锁的线程设置null
        setExclusiveOwnerThread(null);
    }
    // 将c设置给state
    setState(c);
    // 锁资源释放干净true,否则false
    return free;
}

// 唤醒排队的节点
private void unparkSuccessor(Node node) {
    // 拿到头节点状态
    int ws = node.waitStatus;
    if (ws < 0)
        // 先基于CAS,将节点状态从-1,改为0
        compareAndSetWaitStatus(node, ws, 0);
    // 拿到头节点的后续节点。
    Node s = node.next;
    // 如果后续节点为null或者,后续节点的状态为1,代表节点取消了。
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            // 从后往前找到状态小于等于0的节点
            // 找到离head最新的有效节点,并赋值给s
            if (t.waitStatus <= 0)
                s = t;
    }
    // 只要找到了这个需要被唤醒的节点,执行unpark唤醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

4.3.取消节点流程分析

4.3.1概述

在这里插入图片描述

如上图,假设双向队列中线程C,不需要执行任务了,执行了cancelAcquire 方法
1、线程C对应节点 thread 设置为 null
2、往前找到有效节点作为当前节点的 prev
3、将 waitStatus 设置为 1,代表取消
4、脱离 AQS 队列

4.3.2 cancelAcquire流程分析

// 取消在AQS中排队的Node
private void cancelAcquire(Node node) {
    // 如果当前节点为null,直接忽略。
    if (node == null)
        return;
    //1. 线程设置为null
    node.thread = null;

    //2. 往前跳过被取消的节点,找到一个有效节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    //3. 拿到了上一个节点之前的next
    Node predNext = pred.next;

    //4. 当前节点状态设置为1,代表节点取消
    node.waitStatus = Node.CANCELLED;

    // 脱离AQS队列的操作
    // 当前Node是尾结点,将tail从当前节点替换为上一个节点
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // 到这,上面的操作CAS操作失败
        int ws = pred.waitStatus;
        // 不是head的后继节点
        if (pred != head &&
            // 拿到上一个节点的状态,只要上一个节点的状态不是取消状态,就改为-1
            (ws == 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
    }
}

5.应用场景

AQS的核心价值是提供了模板方法,将同步状态的原子性管理与线程解耦,开发者只需关注状态逻辑的装换即可实现高效的同步器。
其实现的同步器有:

  1. ReentrantLock(需要重入互斥时,可用于多线程同时修改共享资源)
  2. Semaphore(限制并发线程数、多线程任务等待统一事件触发)
  3. CountDownLatch(限制并发线程数、多线程任务等待统一事件触发)
  4. CyclicBarrier
  5. ReentrantReadWriteLock (读多写少的场景)
  6. ConditionObject
  7. 可自定义同步器

6.常见问题

为什么AQS选择使用双向队列,而不是单向队列?

AQS中一般是存放没有获取到资源的Node,在竞争锁资源时,ReentrantLock提供了一个方法,lockInterruptibly方法,也就是线程在竞争锁资源的排队途中,允许中断。中断后会执行cancelAcquire方法,从而将当前节点状态置位1,并且从AQS队列中移除掉。如果采用单向链表,当前节点只能按到后继或者前继节点,这样是无法将前继节点指向后继节点的,需要遍历整个AQS从头或者从尾去找。单向链表在移除AQS中排队的Node时,成本很高。

当前在唤醒后继节点时,如果是单向链表也会出问题,因为节点插入方式的问题,导致只能单向的去找有效节点去唤醒,从而造成很多次无效的遍历操作,如果是双向链表就可以解决这个问题。

7.总结

AQS 凭借其出色的模板方法,可以衍生出很多方便且使用的JUC 工具,并且易于使用,方便拓展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cxinxian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值