深入理解 AbstractQueuedSynchronizer(AQS)【源码分析】

前言:

上一篇我们分析了 ReentrantLock 的原理,发现其中的大量实现依赖同步器 AbstractQueuedSynchronizer,也是我们常说的 AQS,AbstractQueuedSynchronizer 到底是个什么神奇的东西,本篇我们将深入分析 AbstractQueuedSynchronizer 源码。

ReentrantLock 传送门:

深入理解 ReentrantLock 【源码分析】

什么是 AbstractQueuedSynchronizer?

AbstractQueuedSynchronizer(AQS)是 java.util.concurrent.locks目录下的一个类,用来构建锁和同步器的框架,使用 AbstractQueuedSynchronizer 能简单高效的构造出同步器,前文聊到的 ReentrantLock,以及 ReentrantReadWriteLock、Semaphore、CountDownLatch 等都是基于 AbstractQueuedSynchronizer 实现的,我们也可以利用 AbstractQueuedSynchronizer 轻松的实现出符合自己需求的同步器。

AbstractQueuedSynchronizer 的原理:

AbstractQueuedSynchronizer 依赖 state、CAS、CHL 实现了同步。

  • state:AbstractQueuedSynchronizer 的一个 int 成员变量,使用 volatile 修饰,保证可见性,标识共享资源的状态,state为 0 标识共享资源没有被占用,state 大于 0 表示共享资源被占用,state 小于 0 则会抛出异常。
  • CAS:AbstractQueuedSynchronizer 中使用 CAS 来操作 state 变量,确保 state 的改变是安全的。
  • CHL:AbstractQueuedSynchronizer 并没有使用原始 CHL 队列,而是基于原始 CHL 队列做了一些变化,CHL是 AbstractQueuedSynchronizer 实现同步的非常核心的一环,CHL是一种 FIFO(先进先出)的逻辑队列(也有叫虚拟队列),多线程竞争共享资源的时候,没有获取到资源的线程会进入 CHL 队列的队尾排队等待获取锁(每个线程就是 CHL 队列的一个 Node)。
    CHL 这种队列据说是由 Craig、Landin 和 Hagersten 三个人一起发明的,因此命名为 CLH 队列。

AbstractQueuedSynchronizer 使用的 CHL 队列基于原始 CHL 队列的一种实现,原始 CHL 队列一般用于自旋锁,而,AbstractQueuedSynchronizer 实现的 CHL 的队列基于原始 CHL 队列先让获取不到共享资源的线程做了一段时间的自旋,然后就让线程挂起,简单来说就是 AbstractQueuedSynchronizer 中的 CHL 队列解决了每个线程无限自旋的问题。

CHL 队列简图:

在这里插入图片描述

AbstractQueuedSynchronizer 中 CHL 队列的 Node 源码分析:

static final class Node {
        //共享节点的标识
        static final Node SHARED = new Node();
        //独占节点的标识
        static final Node EXCLUSIVE = null;

        //节点状态为取消状态 取消状态的节点只能等待被队列移除
        static final int CANCELLED =  1;
        //节点为 SIGNAL 状态 则其后驱节点等待被唤醒 也可以理解为当前节点将要释放锁 需要唤醒后续节点
        static final int SIGNAL    = -1;
        //条件节点 不会位于同步节点中
        static final int CONDITION = -2;
        //共享模式才有的状态
        static final int PROPAGATE = -3;

        //节点的等待状态 默认0 代表初始化状态
        volatile int waitStatus;

        //前驱节点
        volatile Node prev;

        //后驱节点
        volatile Node next;

        //当前节点的线程
        volatile Thread thread;

        //下一个节点
        Node nextWaiter;

        //是否是共享节点是就返回 true
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        //返回前一个节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

		//构造方法
        Node() {    // Used to establish initial head or SHARED marker
        }
		
		//构造方法  addWaiter 方法使用
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

		//构造方法  Condition 条件是时候使用
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

注意:AbstractQueuedSynchronizer 队列的头节点 node 是虚拟节点,后面会解答为什么是虚拟节点。

AbstractQueuedSynchronizer 核心方法分析:

acquire 方法分析:

在 ReentrantLock 篇章中我们提到了 acquire 方法是加锁的核心方法,同时也是 AbstractQueuedSynchronizer 的核心方法,下面我们来分析一下 acquire 方法。

acquire 方法调用链路分析:

在这里插入图片描述

acquire 方法源码分析:

//获取锁的方法
 public final void acquire(int arg) {
		//这里分为三个方法 tryAcquire 、acquireQueued、addWaiter 
		//tryAcquire 先尝试直接获取锁 看是否能够成功获取
		//addWaiter 方法是把当前线程包装成一个 node 节点加入到 CHL 队列 是acquireQueued  addWaiter方法的返回值是 acquireQueued  方法的一个参数
		//tryAcquire 不能成功获取锁后  调用acquireQueued 获取锁 这个方法一定能够成功获取锁(程序不出异常的情况)
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquire 方法是一个极其简单的方法,主要是包装了 tryAcquire 、acquireQueued、addWaiter、 selfInterrupt 四个方法,接下来我们逐步分析这个三个方法。

tryAcquire 方法这里不做分析了,我们知道 tryAcquire 方法实际调用的是 NonfairSync(非公平锁) 或者 FairSync(公平锁) 的 tryAcquire 方法,在 ReentrantLock 篇章中已经详细分析过,ReentrantLock 源码分析传送门如下:

addWaiter 源码分析:

//为当前线程创建独占节点或者共享节点并将其放到队列尾部 Node.EXCLUSIVE 为独占节点 Node.SHARED 为共享 节点
private Node addWaiter(Node mode) {
		//为当前线程创建一个新的节点 
		//mode 有两个值 分别代表两种不同模式 Node.EXCLUSIVE 为独占节点 Node.SHARED 为共享 节点 
        Node node = new Node(Thread.currentThread(), mode);
        //获取队列尾部节点 我理解为临时存储
        Node pred = tail;
		//判断尾部节点是否为空
        if (pred != null) {
			//尾部节点不为空 将刚刚创建的节点的前驱节点指向尾节点
            node.prev = pred;
			//使用 CAS 用设置刚刚创建的节点为尾节点
            if (compareAndSetTail(pred, node)) {
				//设置成功后将刚刚创建的节点作为之前的尾节点的后驱节点
                pred.next = node;
                return node;
            }
        }
		//尾节点为空 说明队列没有创建 需要创建一个空的头节点 并把刚刚创建的节点作为队列尾节点
        enq(node);
        return node;
    }

//初始化队列 并把node 节点加入队列作为队列的尾节点
private Node enq(final Node node) {
		//自旋
        for (;;) {
			//把尾节点 赋值给 t
            Node t = tail;
			//判断尾节点是否为空
            if (t == null) { // Must initialize
				//尾节点为空 创建一个新的空节点 并使用 CAS 将创建的空节点设置为队列头节点
                if (compareAndSetHead(new Node()))
					//头节点设置成功后 也就是队列初始化成功后 把队列的尾节点也指向刚刚创建的空的头节点
                    tail = head;
            } else {
				//尾节点不为空 表示队列初始化完成了 至少是第二次循环了  将node节点的前驱指针指向当前队列的尾节点
                node.prev = t;
				//使用 CAS 用设置node 节点为新的尾节点
                if (compareAndSetTail(t, node)) {
					//设置成功后 将之前的尾节点 t 的后驱指针指向 node 节点
                    t.next = node;
					//node 节点成功加入到队列尾部 
                    return t;
                }
            }
        }
    }

通过对 addWaiter 方法的源码分析,我们知道该方法其实就是把当前线程封装成一个 Node 节点加入队列。

acquireQueued 源码分析:

//获取共享资源 即锁
final boolean acquireQueued(final Node node, int arg) {
		//是否获取成功的标识
        boolean failed = true;
        try {
			//线程是否中断的标识
            boolean interrupted = false;
			//自旋获取锁
            for (;;) {
				//获取当前节点的前一个节点 Node  源码中我们分析过
                final Node p = node.predecessor();
				//判断当前节点的前一个节点是否是头节点 如果是 再次调用 tryAcquire 方法尝试获取锁
				// tryAcquire 方法实际调用的是 NonfairSync(非公平锁) 或者 FairSync(公平锁) 的  tryAcquire 方法 之前有分析过 这里不做分析
                if (p == head && tryAcquire(arg)) {
					//当前节点的前一个节点是头节点 且当前节点 tryAcquire 获取锁成功 则设置队列头节点为当前节点
                    setHead(node);
					//因为当前节点已经是头节点了 那之前节点要从队列中移除了 设置之前的头节点的下一个节点为空  方便JVM 进行GC 操作
                    p.next = null; // help GC
					//设置获取成功标志位flase 这里设置 false 是因为线程没有打断 无需执行 acquire 方法中的 selfInterrupt 方法
                    failed = false;
					//返回打断标志位false
                    return interrupted;
                }
				//当前节点的前一个节点不是头节点 或者 tryAcquire 获取锁失败 需要判断当前节点是否需要被阻塞 防止一直进行自旋 浪费CPU 资源
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
					//能进入这里 表示线程被打断过 设置打断标识为 true
                    interrupted = true;
            }
        } finally {
            if (failed)
				//程序异常 才会走到这里  cancelAcquire 方法就是把 node 状态标记为取消状态
                cancelAcquire(node);
        }
    }

acquireQueued 方法自旋获取锁,但是有不是一直进行自旋,必要的時候会把线程挂起,会根据 shouldParkAfterFailedAcquire 方法的结果那判断是否要继续自旋还是挂起线程,如果需要挂起线程则执行 parkAndCheckInterrupt 。

shouldParkAfterFailedAcquire 源码分析:

//根据当前节点的前驱节点 判断当前节点是否应该被阻塞
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		//当前节点的前驱节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 根据 Node  源码中的状态解析 我们知道当前节点的前驱节点处于唤醒状态 
            return true;
        if (ws > 0) {
           // 根据 Node  源码中的状态解析 我们知道 前驱节点是取消状态 我们需要在队列中移除它 并循环移除它前面的节点 并找到有效的前驱节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //当前节点的前驱节点既不是唤醒状态 也不是取消节点 那就 CAS 设置当前节点的前驱节点状态为 Node.SIGNAL 阻塞状态
			//此时前驱节点的状态 只能是0  或者是  PROPAGATE -3
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
		//不阻塞
        return false;
    }

通过分析 shouldParkAfterFailedAcquire 源码,发现当前节点的前驱节点既不是唤醒状态也不是取消节点,为什么不直接返回 true,结束自旋,而是返回 false,机选进行下一次循环呢?
这里我们分析一下,前驱节点 waitStatus 不是 Node.SIGNAL 状态,也不是取消状态,那前驱节点的状态只能为 0 或者 PROPAGATE时,waitStatus 为 0 只能是正在释放锁的节点或者是新节点,如果是正在释放锁的节点,那我们再次循环一次就可以拿到锁了,如果是新节点我们再循环一次也可以正常阻塞,如果是 waitStatus 是 PROPAGATE 状态,只存在共享状态下,且只有头节点才有这个状态,同样我们再次循环一次就可以获取到锁,多循环一次的开销远小于线程阻塞唤醒的开销,所以这里才返回 false,不让线程挂起。

cancelAcquire 方法源码分析:

//设置节点状态为取消节点
private void cancelAcquire(Node node) {
       //为空判断
        if (node == null)
            return;

		//设置当前节点的所属线程为空
        node.thread = null;

        //获取当前节点的前驱节点
        Node pred = node.prev;
		//前驱节点状态大于0 表示取消节点 一直找到有效的前驱节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

		//获取当前节点的 前驱节点的 后驱节点
        Node predNext = pred.next;

		//设置当前节点的状态为 取消状态
        node.waitStatus = Node.CANCELLED;
    
		
        if (node == tail && compareAndSetTail(node, pred)) {
		//如果当前节点是尾节点 且 CAS 操作将当前节点的前驱节点设置为 尾节点成功  则 CAS 操作将当前节点的前驱节点的后驱节点设置为 null
            compareAndSetNext(pred, predNext, null);
        } else {
		//如果当前节点是尾节点 且 CAS 操作将当前节点的前驱节点设置为 尾节点失败
           //节点状态
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
				//当前节点的前驱节点不是头节点
				//1 当前节点的前驱节点状态为阻塞
				//2 当前节点的前驱节点状态小于0 且将前驱节点的状态设置为 Node.SIGNAL
				//1  2 有一个为 true 在判断当前节点的前驱节点线程是否为空
				//获取当前节点的后驱节点
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
					//当前节点的后驱节点不为空 且状态小于0
					//CAS 操作把当前节点的前驱节点的后驱节点设置为当前节点的后驱节点
                    compareAndSetNext(pred, predNext, next);
            } else {
				//如果当前节点就是head 节点的后驱节点 或者不满足上述条件 就唤醒当前节点的后面的节点
                unparkSuccessor(node);
            }

			//被设置为取消的节点的后驱节点指向自己 方便JVM内存回收
            node.next = node; // help GC
        }
    }

通过我们对 cancelAcquire 方法源码的分析,知道 cancelAcquire 的作用就是把传入的节点设置为 CANCELLED 状态,并通过 unparkSuccessor 方法唤醒后续节点。

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) {
			//当前节点的后驱节点为空 或者 后驱节点状态大于 0  其实就是取消状态 无效节点
            s = null;
			//从队列尾部开始遍历 直到找到最近的一个有效节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
					//状态小于 0 表示是有效的节点
                    s = t;
        }
        if (s != null)
			//唤醒找到的后驱节点
            LockSupport.unpark(s.thread);
    }

unparkSuccessor 方法就是找到当前节点的下一个有效节点并唤醒它。

parkAndCheckInterrupt 方法源码解析:

//挂起当前线程  返回线程中断状态
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

parkAndCheckInterrupt 方法逻辑很简单,就是挂起当前线程,返回挂起状态。

selfInterrupt 方法源码解析:

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

线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止,在整个流程中,并不响应中断,只是记录中断记录,最后抢到锁返回了,那么如果被中断过的话,就需要利用 selfInterrupt 方法补充一次中断,让调用者线程感知到被中断过。

AbstractQueuedSynchronizer 的解锁方法 release 源码分析:

在 ReentrantLock 篇章中我们说不管公平锁还是非公平锁最终调用的解锁方法都是 AbstractQueuedSynchronizer 的 release 方法来实现的,我们来分析一下 release 方法。

//独占模式解锁	
public final boolean release(int arg) {
		//tryRelease 调用的是 ReentrantLock 的 tryRelease 方法 将 state 设置为0 并设置当前线程不占有锁 在 ReentrantLock 篇章中有分析过
        if (tryRelease(arg)) {
			//tryRelease 返回true 表示解锁成功  且锁没有被其他线程占有
            Node h = head;
            if (h != null && h.waitStatus != 0)
				//头节点不为空 且状态不是0 0表示初始化节点  唤醒头节点后面的节点 unparkSuccessor 方法上面分析过
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

release 方法一共做了两个操作如下:

  • 调用 ReentrantLock 的 tryRelease 方法 将 state 设置为 0,并设置当前线程不再占用锁
  • 调用 unparkSuccessor 方法唤醒后面的有效节点。

AbstractQueuedSynchronizer 的队列的头节点为虚拟节点,为什么要虚拟这个节点?

我们知道 Node 节点中有一个 waitStatus 变量,来标识节点的状态,通过上面的源码分析我们知道这其中有一个很重要的状态 SIGNAL ,该状态标识着我们是否需要需要唤醒下一个节点,也就是说每个节点在挂起休眠前都需要将他的前置节点状态标记为 SIGNAL 状态,这样就必须要要有一个前置节点,前置节点其实就是持有锁的节点,释放锁之后需要唤醒下一个节点,那第一个节点的前置节点是谁?最终采用的方式就是创建一个节点,也就是虚拟一个节点。

如有错误的地方欢迎指出纠正。

  • 83
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的AQSAbstractQueuedSynchronizer)是实现同步器的一种重要工具。在AQS中,一个节点表示一个线程,依次排列在一个双向队列中,同时使用CAS原子操作来保证线程安全。当多个线程对于同一资源竞争时,一个节点会被放置在队列的尾部,其他线程则在其之前等待,直到该资源可以被定。 当一个线程调用lock()方法进行定时,它会首先调用tryAcquire()方法尝试获取。如果当前资源尚未被定,则该线程成功获取,tryAcquire()返回true。如果当前资源已被定,则线程无法获取,tryAcquire()返回false。此时该线程就会被加入到等待队列中,同时被加入到前一个节点的后置节点中,即成为它的后继。然后该线程会在park()方法处等待,直到前一个节点释放了,再重新尝试获取。 在AQS中,当一个节点即将释放时,它会调用tryRelease()方法来释放,并唤醒后置节点以重试获取。如果当前节点没有后置节点,则不会发生任何操作。当一个线程在队列头部成功获取和资源时,该线程需要使用release()方法释放和资源,并唤醒等待队列中的后置节点。 总之,AQS中的机制是通过双向等待队列实现的,其中节点表示线程,使用CAS原子操作保证线程安全,并在tryAcquire()和tryRelease()方法中进行定和释放。该机制保证了多线程环境下资源的正确访问和线程的安全执行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值