IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> AbstractQueuedSynchronizer源码解析(下) -> 正文阅读

[Java知识库]AbstractQueuedSynchronizer源码解析(下)

下面让我们步入正轨,谈谈如何获取锁以及释放锁。

  1. 独占式(排他式)获取同步状态:acquire(), addWaiter(), enq(), acquireQueued();
  2. 独占式释放同步状态:release();
  3. 共享式获取同步状态:acquireShared(), doAcquireShared(), setHeadAndPropagate();
  4. 共享式释放同步状态:releaseShared(), doReleaseShared();

请添加图片描述
这个图总结了 AQS 整体架构的组成,和部分场景的动态流向,图中两个点说明一下,方便大家观看。

  1. AQS 中队列只有两个:同步队列 + 条件队列,底层数据结构两者都是链表(在上一篇我们已经介绍过了);
  2. 图中有四种颜色的线代表四种不同的场景,1、2、3 序号代表看的顺序。

AQS 本身就是一套锁的框架,它定义了获得锁和释放锁的代码结构,所以如果要新建锁,只要继承 AQS,并实现相应方法即可。

一、获取锁

1.1 acquire排他锁

获取锁最直观的感受就是使用 Lock.lock () 方法来获得锁,最终目的是想让线程获得对资源的访问权。

Lock 一般是 AQS 的子类,lock 方法根据情况一般会选择调用 AQS 的 acquire 或 tryAcquire 方法

acquire 方法 AQS 已经实现了,tryAcquire 方法是等待子类去实现。
acquire 方法制定了获取锁的框架,先尝试使用 tryAcquire 方法获取锁,获取不到时,再入同步队列中等待锁。tryAcquire 方法 AQS 中直接抛出一个异常,表明需要子类去实现,子类可以根据同步器的 state 状态来决定是否能够获得锁,接下来我们详细看下 acquire 的源码解析。

acquire 也分两种,一种是排它锁,一种是共享锁,我们一一来看下:

	// 排他模式下,尝试获得锁
	public final void acquire(int arg) {
		// tryAcquire方法需要实现类去实现
		// 实现思路一般都是 cas 给 state 赋值来决定是否能获得锁
        if (!tryAcquire(arg) && 
            // acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 注意:上面这行代码是两个方法addWaiter和acquireQueued,这两个方法在下面我们会细说
        	// addWaiter入参代表是排他模式
        	acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

以上代码的主要步骤是(流程见整体架构图中红色场景):

  1. 尝试执行一次 tryAcquire,如果成功直接返回,失败走 2;
  2. 线程尝试进入同步队列,首先调用 addWaiter 方法,把当前线程放到同步队列的队尾;
  3. 接着调用 acquireQueued 方法,两个作用,第一个作用是阻塞当前节点,第二个作用是节点被唤醒时,使其能够获得锁;
  4. 如果 2、3 失败了,打断线程。

1.1.2 addWaiter

接下来我们先来看下 addWaiter 的源码实现:

方法的主要目的:node追加到同步队列的队尾;

	// 方法传入的参数 mode 表示 当前线程的节点
	// return node 表示新增的node
	/**
     * 主要思路:新node.pre = 队尾  队尾.next = 新node
     */
    private Node addWaiter(Node mode) {
    	// 初始化node
        Node node = new Node(Thread.currentThread(), mode);
        // 这里的逻辑与 enq 方法的逻辑一样,只不过 enq 增加对队尾判空的操作
        // 当前节点的前置节点为tail
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // 以CAS的方式去设置尾节点 compareAndSetTail
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 有可能CAS会失败,因为存在竞争,所以进入 enq 环节
        // enq 自旋保证node加入队尾
        enq(node);
        return node;
    }

	// 线程加入同步队列队尾的方法
	// 这里注意一下,返回值是添加 node 的前一个节点
    private Node enq(final Node node) {
    	// 自旋,直到成功为止,或者直到放弃为止
        for (;;) {
        	// 得到队尾节点
            Node t = tail;
            // 如果队尾为空,则说明当前同步队列没有进行初始化,进行初始化
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            // 如果队尾不为空,则将当前节点追加到队尾
            } else {
                node.prev = t;
                // node追加到队尾
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

总结:就是把新的节点追加到同步队列的队尾。

1.1.3 acquireQueued

下一步就是要阻塞当前线程了,是 acquireQueued 方法来实现的,即队列中的节点什么时候阻塞,什么时候唤醒由 acquireQueued 去决定,我们来看下源码实现:

这个方法主要做了两件事:

  1. 通过不断的自旋尝试使自己的前一个节点的状态变成signal状态,然后阻塞自己;
  2. 获得锁的线程执行完毕之后,释放锁时,会把阻塞的 node 唤醒,node 唤醒之后再次自旋,尝试获得锁;
  3. 返回 false 表示获得锁成功,返回 true 则表示失败
	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
            	// 选择上一个节点,p 代表前置结点
                final Node p = node.predecessor();
                // 有两种情况会走到 p == head
                // 1. node之前没有获得锁,进入 acquireQueued 方法时,才发现它的前置节点就是头节点,于是尝试获得一次锁
                // 2. node之前一直在阻塞沉睡,然后被唤醒,此时唤醒 node 的节点正是其前一个节点,也能走到if
                // 如果自己 tryAcquire (尝试抢锁) 成功,就立刻讲自己设置成为 head,并且把上一个节点移除
                // 如果自己 tryAcquire 失败,尝试进入同步队列
                if (p == head && tryAcquire(arg)) {
                	// 获得锁,将自己设置成 head 节点
                    setHead(node);
                    // p 被回收
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire 把 node 的前一个节点设置为signal
                // 只要前一个节点的状态是 signal 了,那么自己就可以阻塞了
                if (shouldParkAfterFailedAcquire(p, node) &&
                	// 线程是在这个方法中阻塞的,醒来的时候仍在无限 for 循环里面,就能再次自旋尝试获得锁
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        	// 如果获得 node 的锁失败,将 node 从队列中移除
            if (failed)
                cancelAcquire(node);
        }
    }

1.1.4 shouldParkAfterFailedAcquire

shouldParkAfterFailedAcquire,这个方法的主要目的就是把前一个节点的状态置为 SIGNAL,只要前一个节点的状态是 SIGNAL,当前节点就可以阻塞了。

	// 当前线程可以安心阻塞的标准,就是前一个节点的线程状态是 signal 了
	// 传入的参数 pred 表示前一个节点, node 表示当前节点
	/* 
	 * 关键操作:
	 * 1. 确认前一个节点是否有效。无效的话,一直往前寻找状态不是取消的节点
	 * 2. 把前一个节点的状态设置为 signal
	 * 1、2这两步的操作,有可能一次就成功,也有可能需要外部循环多次才能成功(外面是多个for循环)
     */
	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 如果前一个节点 waitStatus 状态已经是 signal 了,直接返回,不需要再自旋了
        if (ws == Node.SIGNAL) 
            return true;
        if (ws > 0) {
        	// 找到前一个状态不是取消的节点,因为把当前 node 挂在有效节点的身上
        	// 因为节点的状态是取消的话,是无效的,是不能作为 node 的前置节点的,所以必须找到 node 的有效节点才行
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        // 否则直接把节点状态设置为 signal
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

acquire 整个过程非常长,代码也非常多,但注释很清楚,可以一行一行仔细看看代码。

1.1.5 总结

acquire 方法大致分为三步:

  1. 使用 tryAcquire 方法尝试获得锁,获得锁直接返回,获取不到锁的走 2;
  2. 把当前线程组装成节点(Node),追加到同步队列的尾部(addWaiter);
  3. 自旋,使同步队列中当前节点的前置节点状态为 signal 后,然后阻塞自己。

1.2 acquireShared 获取共享锁

acquireShared 整体流程和 acquire 相同,代码也很相似,重复的源码就不贴了,我们就贴出来不一样的代码来,也方便大家进行比较:

  1. 第一处尝试获得锁的地方,有所不同,排它锁使用的是 tryAcquire 方法,共享锁使用的是 tryAcquireShared 方法,如下图:
    在这里插入图片描述

  2. 第二处不同,在于节点获得排它锁时,仅仅把自己设置为同步队列的头节点即可(setHead 方法),但如果是共享锁的话,还会去唤醒自己的后续节点,一起来获得该锁(setHeadAndPropagate 方法),不同之处如下(左边排它锁,右边共享锁):
    在这里插入图片描述

1.2.1 setHeadAndPropagate

这个方法主要做了两件事:

  1. 把当前节点设置成头节点
  2. 看看后续节点有无正在等待,并且也是共享模式的,有的话唤醒这些节点
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    // 当前节点设置成头节点
    setHead(node);
    // propagate > 0 表示已经有节点获得共享锁了
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 共享模式,还唤醒头节点的后置节点
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

// 释放后置共享节点
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 还没有到队尾,此时队列中至少有两个节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果队列状态是 SIGNAL ,说明后续节点都需要唤醒
            if (ws == Node.SIGNAL) {
                // CAS 保证只有一个节点可以运行唤醒的操作
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;           
                // 进行唤醒操作
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;              
        }
        if (h == head)                
            break;
    }
}

这个就是共享锁独特的地方,当一个线程获得锁后,它就会去唤醒排在它后面的其它节点,让其它节点也能够获得锁。

二、释放锁

释放锁的触发时机就是我们常用的 Lock.unLock () 方法,目的就是让线程释放对资源的访问权(流程见整体架构图蓝色路线)。
释放锁也是分为两类,一类是排它锁的释放,一类是共享锁的释放,我们分别来看下。

2.1 释放排它锁

排它锁的释放就比较简单了,从队头开始,找它的下一个节点,如果下一个节点是空的,就会从尾开始,一直找到状态不是取消的节点,然后释放该节点,源码如下:

2.1.1 release

    public final boolean release(int arg) {
    	// tryRelease 交给实现类去实现,如果返回true则说明成功释放锁
        if (tryRelease(arg)) {
            Node h = head;
            // 头节点不为空 && 非初始化状态
            if (h != null && h.waitStatus != 0)
            	// 从头开始唤醒等待锁的节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

2.1.2 unparkSuccessor

	// 当线程释放锁成功后,从 node 开始唤醒同步队列中的节点
	// 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
	private void unparkSuccessor(Node node) {
		// node节点为当前释放锁的节点,也是同步队列的头结点
        int ws = node.waitStatus;
        // 如果节点已经被取消了,把节点的状态设置为初始化
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 拿出 node 节点的下一个节点
        Node s = node.next;
        // s 为空,表示 node 的后一个节点为空
        // s.waitStatus > 0 表示s节点已经被取消了
        // 遇到上面两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 这个for循环,从尾部开始迭代
            // 主要是因为节点被阻塞的时候,是在acquiredQueued方法里面被阻塞的(上面已经介绍了这个方法)
            // 所以唤醒的时候也一定是在acquiredQueued方法里面被唤醒
         	// 唤醒的条件是,判断判断当前节点的前置节点是否为头结点,这里是判断当前节点的前置节点
         	// 所以这里必须从尾部开始迭代,目的就是过滤无效的前置节点,不然节点被唤醒时,发现前置节点是无效节点的话,就又会陷入阻塞
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 唤醒以上代码找到的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

2.2 释放共享锁

2.2.1 releaseShared

释放共享锁的方法是 releaseShared,主要分成两步:

  1. tryReleaseShared 尝试释放当前共享锁,失败返回 false,成功走 2;
  2. 唤醒当前节点的后续阻塞节点,这个方法我们之前看过了,线程在获得共享锁的时候,就会去唤醒其后面的节点,方法名称为:doReleaseShared。

我们一起来看下 releaseShared 的源码:

	// 共享模式下,释放当前线程的共享锁
	public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
        	// 线程在获得共享锁的时候,就会去唤醒其后面的节点
            doReleaseShared();
            return true;
        }
        return false;
    }

三. 条件队列的一些重要方法

请添加图片描述

3.1 为什么需要条件队列

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对
象关联的锁。Condition对象是由Lock对象创建出来的,换句话说,Condition是依赖Lock对象的。
当调用Condition的await()方法后,当前线程会释放锁并在此等待。而其他线程调用Condition对象的
signal()方法通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
因为并不是所有场景一个同步队列就可以搞定的。

  1. 在遇到锁 + 队列结合的场景时,就需要 Lock + Condition 配合才行,先使用 Lock 来决定哪些线程可以获得锁,哪些线程需要到同步队列里面排队阻塞;
  2. 获得锁的多个线程在碰到队列满或者空的时候,可以使用 Condition 来管理这些线程,让这些线程阻塞等待,然后在合适的时机后,被正常唤醒。

同步队列 + 条件队列联手使用的场景,最多被使用到锁 + 队列的场景中。所以说条件队列也是不可或缺的一环。

接下来我们来看一下条件队列一些比较重要的方法,以下方法都在 ConditionObject 内部类中。

3.2 入队列等待 await

获得锁的线程,如果在碰到队列满或空的时候,就会阻塞住,这个阻塞就是用条件队列实现的,这个动作我们叫做入条件队列,方法名称为 await,流程见整体架构图中绿色箭头流向,我们一起来看下 await 的源码:

		// 线程进入条件队列
		public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 加入条件队列的队尾
            Node node = addConditionWaiter();
            // 加入队列后,会释放 lock 时所申请的资源,唤醒同步队列的头结点
            // fullyRelease 释放资源,自己马上就要阻塞了,所以要马上释放之前 lock 的资源
            // 不然自己不被唤醒的话,别的线程永远得不到该共享的资源
            long savedState = fullyRelease(node);
            int interruptMode = 0;
            // 确认 node 不在同步队列上,在阻塞,如果node在同步队列上的话,是不能够上锁的
            // 为什么这么做?
            // node刚被加入到条件队列,立马就被其他线程唤醒转移到同步队列当中了
            // 线程之前在条件队列中沉睡,被唤醒后加入到同步队列当中
            while (!isOnSyncQueue(node)) {
            	// 阻塞在条件队列上
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中
            // 所以这里尝试acquireQueued
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) 
            	// 如果状态不是CONDITION,就会自动删除
            	// CONDITION表示线程正在等待条件
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

await 方法有几点需要特别注意:

  1. fullyRelease(node);,节点在准备进入条件队列之前,一定会先释放当前持有的锁,不然自己进去条件队列了,其余的线程都无法获得锁了;
  2. (acquireQueued(node, savedState) && interruptMode != THROW_IE),此时节点是被 Condition.signal 或者 signalAll 方法唤醒的,此时节点已经成功的被转移到同步队列中去了(整体架构图中蓝色流程),所以可以直接执行 acquireQueued 方法;
  3. Node 在条件队列中的命名,源码喜欢用 Waiter 来命名,所以我们在条件队列中看到 Waiter,其实就是 Node。

await 方法中有两个重要方法:addConditionWaiter 和 unlinkCancelledWaiters,我们一一看下。

3.3 addConditionWaiter

addConditionWaiter 方法主要是把节点放到条件队列中,方法源码如下:

		// 增加新的 waiter 到队列中,返回新添加的 waiter
		// 如果尾节点的状态不是CONDITION状态,删除条件队列中所有状态不是CONDITION的节点
		// 如果队列为空,新增的节点作为队列头节点,否则追加到尾节点上
		private Node addConditionWaiter() {
            Node t = lastWaiter;
         	// 如果尾节点的状态不是CONDITION状态,删除
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 新建条件队列node
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // 如果是空的,直接放在队列头
            if (t == null)
                firstWaiter = node;
            // 队列不为空,放在队列尾
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

整体过程比较简单,就是追加到队列的尾部

3.4 unlinkCancelledWaiters

其中还有个重要方法叫做 unlinkCancelledWaiters,这个方法会删除掉条件队列中状态不是 CONDITION 的所有节点,我们来看下 unlinkCancelledWaiters 方法的源码,如下:

		// 检查尾部的 waiter 是不是已经不是CONDITION状态
		// 如果不是,则会删除
		private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            // trail 表示上一个状态,可以将状态为 CONDITION 的node串联起来,即使node之间有其他的节点也可以访问
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
                // 当前 node 的状态不是CONDITION,则会删除自己
                if (t.waitStatus != Node.CONDITION) {
                	// 删除当前 node
                    t.nextWaiter = null;
                    // 如果 trail 是空的,则循环从头开始,说明当前节点的状态都不是 CONDITION
                    // 都已经被删除了。所以移动队列的头结点到当前节点的下一个节点
                    if (trail == null)
                        firstWaiter = next;
                    // 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉
                    // 然后把自己挂到上一个状态是CONTION的节点上
                    else
                        trail.nextWaiter = next;
                    // 遍历结束,最后一次找到的CONDITION节点就是尾节点
                    if (next == null)
                        lastWaiter = trail;
                }
                // 状态是CONDITION的node
                else
                    trail = t;
                // 继续循环,循环顺序从头到尾
                t = next;
            }
        }

3.5 单个唤醒 signal

signal 方法是唤醒的意思,比如之前队列满了,有了一些线程因为 take 操作而被阻塞进条件队列中,突然队列中的元素被线程 A 消费了,线程 A 就会调用 signal 方法,唤醒之前阻塞的线程,会从条件队列的头节点开始唤醒(流程见整体架构图中黄色部分),源码如下:

		// 唤醒阻塞在条件队列中的节点
		public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 从头节点开始唤醒
            Node first = firstWaiter;
            if (first != null)
            	// doSignal 方法会把条件队列中的节点转移到同步队列中去
                doSignal(first);
        }
		// 把条件队列头节点转移到同步队列去
		private void doSignal(Node first) {
            do {
            	 // nextWaiter为空,说明到队尾了
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                // 从队列头部开始唤醒,所以直接把头节点.next 置为 null,这种操作其实就是把 node 从条件队列中移除了
         		// 这里有个重要的点是,每次唤醒都是从队列头部开始唤醒
         		// 所以把 next 置为 null 没有关系,如果唤醒是从任意节点开始唤醒的话,就会有问题,容易造成链表的割裂
                first.nextWaiter = null;
                // transferForSignal 方法会把节点转移到同步队列中去
         		// 通过 while 保证 transferForSignal 能成功
         		// 等待队列的 node 不用管他,在 await 的时候,会自动清除状态不是 Condition 的节点(通过 unlinkCancelledWaiters 方法)
         		// (first = firstWaiter) != null  = true 的话,表示还可以继续循环, = false 说明队列中的元素已经循环完了
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

我们来看下最关键的方法:transferForSignal。

 // 返回 true 表示转移成功, false 失败
 // 大概思路:
 // 1. node 追加到同步队列的队尾
 // 2. 将 node 的前一个节点状态置为 SIGNAL,成功直接返回,失败直接唤醒
 // 可以看出来 node 的状态此时是 0 了
 final boolean transferForSignal(Node node) {
     /*
      * If cannot change waitStatus, the node has been cancelled.
      */
     // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
     if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
         return false;
 
     // 当前队列加入到同步队列,返回的 p 是 node 在同步队列中的前一个节点
     // 看命名是 p,实际是 pre 单词的缩写
     Node p = enq(node);
     int ws = p.waitStatus;
     // 状态修改成 SIGNAL,如果成功直接返回
     // 把当前节点的前一个节点修改成 SIGNAL 的原因,是因为 SIGNAL 本身就表示当前节点后面的节点都是需要被唤醒的
     if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
         // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒
         LockSupport.unpark(node.thread);
     return true;
 }

整个源码下来,我们可以看到,唤醒条件队列中的节点,实际上就是把条件队列中的节点转移到同步队列中,并把其前置节点状态置为 SIGNAL。

3.6 全部唤醒 signalAll

signalAll 的作用是唤醒条件队列中的全部节点,源码如下:

 public final void signalAll() {
         if (!isHeldExclusively())
             throw new IllegalMonitorStateException();
         // 拿到头节点
         Node first = firstWaiter;
         if (first != null)
             // 从头节点开始唤醒条件队列中所有的节点
             doSignalAll(first);
     }
     // 把条件队列所有节点依次转移到同步队列去
     private void doSignalAll(Node first) {
         lastWaiter = firstWaiter = null;
         do {
             // 拿出条件队列队列头节点的下一个节点
             Node next = first.nextWaiter;
             // 把头节点从条件队列中删除
             first.nextWaiter = null;
             // 头节点转移到同步队列中去
             transferForSignal(first);
             // 开始循环头节点的下一个节点
             first = next;
         } while (first != null);
     }

从源码中可以看出,其本质就是 for 循环调用 transferForSignal 方法,将条件队列中的节点循环转移到同步队列中去。

四、总结

这个图应该看懂了吧,伙计们。2022新年快乐。
请添加图片描述

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-01-03 15:56:35  更:2022-01-03 15:58:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:07:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码