Condition 条件对象源码浅读

Condition 概述

Condition 是 Doug Lea 大佬在 JDK1.5 之后新加入的线程通信工具类,为什么要加入?像之前如果要做线程等待唤醒机制,都要通过 wait()notfiy()/notifyAll(),但是唤醒的方法有缺陷,就是不能具体唤醒哪个线程,非常不灵活,而且如果唤醒所有线程,就又要去尝试竞争锁资源,无疑是一笔 CPU 资源消耗,那么这里就提供了 Condition 工具类相当于重新实现了这个几个方法来解决这个问题。

既然 Condition 可以解决这个问题,那么肯定要保留原有的 wait()nofiy()notifyAll() 功能然后进行功能增强,所以 Conditionawait() 代替 wait()signal() 代替 notify()signalAll() 代替 notifyAll() 方法,这里 Condition 底层借助于 AQS 实现增强,后面源码会分析到。

代码演示

假设现在有三个方法必须按照顺序执行,先用传统的方法实现,代码如下:

public class MonitorDemo {

    int num = 1;

    public synchronized void test1() {
        while (num != 1) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        num++;
        System.out.println(1);
        notifyAll();
    }

    public synchronized void test2() {
        while (num != 2) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(2);
        num++;
        notifyAll();
    }

    public synchronized void test3() {
        while (num != 3) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(3);
        num = 1;
        notifyAll();
    }

    public static void main(String[] args) {
        MonitorDemo demo = new MonitorDemo();
        new Thread(() -> { while (true) demo.test1();}).start();
        new Thread(() -> { while (true) demo.test2();}).start();
        new Thread(() -> { while (true) demo.test3();}).start();
    }
}

输出结果每次都是按照 1、2、3 输出非常有顺序,但是会发现每次执行的线程会需要唤醒所有的等待线程,但是最终只会有一个现成抢到锁,另一个线程就做了一次无用功,耗费了一次 CPU 资源,现在如果能够让执行的线程清晰的知道要唤醒哪个线程,这样就不会多浪费一次 CPU 资源,接下来使用 Condition 进行改造。

换成 Condition 工具类进行改造上面的代码,首先这三个方法的执行阻塞都是有自己的条件,所以这里我们需要给他准备3个 Condition 自身控制条件,每个方法使用自己的条件,代码如下:

public class ConditionDemo {

    int num = 1;
    Lock lock = new ReentrantLock();
    // condition1 给 test1 方法使用
    Condition condition1 = lock.newCondition();
    // condition2 给 test2 方法使用
    Condition condition2 = lock.newCondition();
    // condition3 给 test3 方法使用
    Condition condition3 = lock.newCondition();

    public  void test1() {
        lock.lock();
        while (num != 1) {
            try {
            	// num 不是 1 表示这个方法不能运行,所以等待 test3 叫醒 test1
            	// 注意这里 test3 肯定是叫醒 test1,目标非常明确
                condition1.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        num++;
        System.out.println(1);
        // 执行完 test1,要执行 test2,那就去叫醒 test2 就可以,目标也非常明确
        condition2.signal();
        lock.unlock();

    }

    public void test2() {
        lock.lock();
        while (num != 2) {
            try {
                // num 不是 2,test2 不能运行,所以等待等待着,等待 test 1 去把 test2 叫醒,目标明确
                condition2.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(2);
        num++;
        // test2 执行完,肯定要去唤醒 test3 去执行了,目标明确
        condition3.signal();
        lock.unlock();
    }

    public void test3() {
        lock.lock();
        while (num != 3) {
            try {
                // num 不是 3,停止该线程的运行,等待着 test2 线程唤醒自己
                condition3.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(3);
        num = 1;
        // 执行完直接叫醒 test1,重新开始继续执行,目标非常明确。
        condition1.signal();
        lock.unlock();
    }

    public static void main(String[] args) {
        ConditionDemo demo = new ConditionDemo();

        new Thread(() -> { while (true) demo.test1();}).start();
        new Thread(() -> { while (true) demo.test2();}).start();
        new Thread(() -> { while (true) demo.test3();}).start();
    }
}

可以清晰看到,每次唤醒和等待都是非常明确,知道哪个线程要等待,哪个线程要被唤醒,非常灵活,虽然对之前的 wait()notify()notifyAll() 有所提高,但是有一点不友好的就是,还有伴随同步锁使用,也就是还要加锁,但是后面会有 LockSupport 来继续优化,突破了在锁的这种限制。

对比了这个用法,下面开始来分析 Condition 所涉及到的原码,就用下面这个简化后的例子做分析,主要分析 await()signal() 两个方法,代码如下:

    public  void test1() {
        lock.lock();
        while (num != 1) {
            	// num 不是 1 表示这个方法不能运行,所以等待 test3 叫醒 test1
            	// 注意这里 test3 肯定是叫醒 test1,目标非常明确
                condition1.await();
        num++;
        System.out.println(1);
        // 执行完 test1,要执行 test2,那就去叫醒 test2 就可以,目标也非常明确
        condition2.signal();
        lock.unlock();
    }

首先看 Condition 是个接口,具备所有之前线程同步机制所需要的方法,如图示:

在这里插入图片描述

那是接口就必定需要子类实现该接口,可以看到子类实现是由 AQS 的一个内部类 ConditionObject 实现的,如下图示:

在这里插入图片描述

ConditionObject 类结构

会发现 ConditionObject 维护了一个单向链表结构,firstWaiter 表示第一个节点,lastWaiter 表示最后一个节点,还记得之前在 Node 类中有个属性叫做 nextWaiter,等下这里就会用到。

先回顾下之前讲独占锁的加锁过程,假设锁已经被占用了,还没释放,后面所有想尝试获取锁的线程都必须进入 AQS 的 CLH 排队等待,如下示意图:
在这里插入图片描述
现在假设有个线程 Node1 过来执行 test1() 方法,然后加锁成功,然后运行的时候遇到了这行代码 condition1.await();,进入方法内部,看下是这个方法会让线程干什么去了,代码如下:

        public final void await() throws InterruptedException {
            // 判断当前加锁成功的线程是否要中断退出,如果 true 就直接响应中断请求,直接抛出异常程序结束
            if (Thread.interrupted())
                throw new InterruptedException();
            // Node 节点进入 Condition 队列
            Node node = addConditionWaiter();
            // Node 释放锁成功,相当于 Node 节点从 CLH 队列中出去了
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 判断是否在 CLH 同步队列中,很显然上述代码已经将其出队了
            while (!isOnSyncQueue(node)) {
                // 然后挂起阻塞该线程,所以线程调用了 await() 方法都是在这里被阻塞挂起了
                LockSupport.park(this); 
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 假设阻塞的线程被人唤醒,唤醒了之后就会走这逻辑
            // 这个方法是独占锁模式下,阻塞队列中的线程尝试获取锁的方法
            // 加锁成功,踢出 CLH 队列,加锁失败被阻塞挂起,注意此时阻塞是在另一个地方了,上面的阻塞只是针对调用了 await()方法
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 如果 Condition 队列中有取消的节点,那么就需要重新维护这个 Condition 队列的完整性
            // 下面这个 unlinkCancelledWaiters() 方法就是干这个事情的
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
        

进入到 addConditionWaiter() 方法内部,代码如下:

		private Node addConditionWaiter() {
		    // 把 Con 的尾指针暂时保存给临时变量 t,第一次进来肯定是 null 的,所以 t == null
            Node t = lastWaiter;
            // 该方法是针对异常情况的,比如在 Condition 队列中有取消的 Node 节点,
            // 此时需要维护队列结构的完整性,这段代码就是干这件事的,现在我们都是按照正常情况先分析一遍,
            // 后面再讨论异常情况,所以这个条件 false,继续走后门的代码
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 将当前线程封装成 Condition 类型的 Node 节点,之前的是封装成共享模式或者独占模式
            // 这里注意这个条件队列里的 Node 模式为 Node.CONDITION(-2)
            /* 其他几个状态为:     
        	static final int CANCELLED =  1;
        	static final int SIGNAL    = -1;
        	static final int CONDITION = -2;
        	static final int PROPAGATE = -3;
        	*/   
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // 第一次进来 t == null 成立
            if (t == null)
            	// 直接把 Con 的头指针指向封装好的类型为 Condition 的 Node 节点
                firstWaiter = node;
            else
                // 后面新进来的节点都直接用节点的后驱指向指向新建来的 Node 节点即可
                t.nextWaiter = node;
            // 然后再把 Con 的尾指针指向 封装好的类型为 Condition 的 Node 节点
            // 到此第一次进来的线程就算成功进入了 Con 队列了,注意这个是单向链表结构
            lastWaiter = node;
            return node;
        }

执行完 addConditionWaiter() 方法,会得到一张示意图,如下所示:
在这里插入图片描述

相当于执行完 addConditionWaiter() 方法后,就让这个执行的线程进入到条件队列,注意条件队列是单向队列,然后继续下一行 int savedState = fullyRelease(node); 进入方法内部,如下:

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

从这里面的方法可以看出 Condition 是由独占锁模式下实现的,共享锁是不能这样用的。执行完方法 fullyRelease() ,其实就在执行独占锁释放,我们在读独占锁源码解读时,已经知道了,在释放成功之后会把节点踢出 Node,然后再唤醒下一个线程,这里就是把这个 Node 节点从 CLH 队列中踢出队列,然后唤醒下一个线程,主要是干了这两件事,示意图如下:
在这里插入图片描述
从示意图可以看出,Node1 被踢出了 CLH 队列,然后 Node3 会被唤醒,这两点注意,接着继续分析一下代码:

	// 判断 Node 节点是否要进入 CLH 队列,此时的 Node 为 Node1,不再 CLH 队列中了
    while (!isOnSyncQueue(node)) {
    	// 然后该 Node 节点就挂起在这里了,需要等待其他线程唤醒它
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 判断队列是否在 CLH 队列中,很显然不在,Node 现在实在 Condition 队列中
    final boolean isOnSyncQueue(Node node) {
        // 此时 Node 一开始初始化就把 waitStatus 设置成了 Node.CONDITION,所以这里返回 false
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        
        return findNodeFromTail(node);
    }

然后假设被唤醒的 Node3 线程过来,调用 signal() 方法,进入 signal() 内部,代码如下:

        public final void signal() {
            // 判断是否为独占锁模式,如果不是抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 从头指针向的节点开始出队
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

进入 doSignal() 方法内部,代码如下:

		// 这个方法主要去唤醒被 await 阻塞的线程。
        private void doSignal(Node first) {
            do {
                // 把 firstWaiter 头指针往后移动到后驱节点,因为当前节点要准别出队了
                // 如果 first.nextWaiter == null 成立,表示当前只有一个节点,这样 Condition 的两个指针都指向这同一个节点
                if ( (firstWaiter = first.nextWaiter) == null)
                    // 把 Condition 尾指针置 null
                    lastWaiter = null;
                // 然后把后驱指向置成 null,断开引用,因为当前节点要出队了
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

    final boolean transferForSignal(Node node) {
		// 把 waitStatus 修改成 0 ,这里不该成功会一直在这里自旋 外面是个 while 循环
		// 所以最终 Node 的 waitStatus 为 0
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
		// 重新入 CLH 队列,加入到 CLH 对尾排队等着
        Node p = enq(node);
        // 此时 ws = 0,因为上述代码保证修改成功了 waitStatus 为 0
        int ws = p.waitStatus;
        // 这里会把该 Node 的 waitStatus 修改成可运行的状态,然后就被唤醒
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

上述代码示意图如下:
在这里插入图片描述

就是将 nextWaiter 断开,然后 Node1 有重新进入到 CLH 队列中,然后再把 Condition 对象的 firstWaiter 头指针往后移动,移动到新 Node3 节点上,这样 Node1 就算从 Condition 队列中出队了,示意图如下:

在这里插入图片描述
注意此时的 Node1 节点是挂起在 Conditionawait() 方法中,代码如下:

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);  // Node1 此时被挂起在这里
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

注意此时的 Node1 是阻塞在 Condition.await() 方法,所以被唤醒就需要从这里开始继续往下运行,假设被唤醒了,调用 isOnSyncQueue() 方法判断是否在 CLH 队列中,现在肯定是在的了,因为 Node1 已经重新进入了 CLH 队列,然后执行 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)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

发现是之前独占锁模式下,阻塞队列中的线程尝试加锁代码,加锁成功,直接踢出 CLH 队列,加锁失败,阻塞挂起,等待被唤醒然后再次尝试加锁,如果灭有意外退出,会一直等到加锁成功才会退出,以上就是对 Condition 条件队列的分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

魔道不误砍柴功

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

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

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

打赏作者

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

抵扣说明:

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

余额充值