浅析Condition与等待通知机制

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。Condition是个接口,基本的方法就是await()和signal()方法。


Condition简介

任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制,同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

在这里插入图片描述


Condition方法

Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition(),调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

  1. Conditon中的await()对应Object的wait();
  2. Condition中的signal()对应Object的notify();
  3. Condition中的signalAll()对应Object的notifyAll()

Condition接口中的方法如下所示
在这里插入图片描述


Condition实例

一道面试题如下

要求用三个线程完成如下操作,
线程A打印5次A
线程B打印10次B
线程C打印15次
线程A打印5次A
线程B打印10次B

如此循环打印


使用3个Condition进行线程调度

package cn.wideth.util;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class PrintData {

    private int number = 1;
    private ReentrantLock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print5() throws InterruptedException {
        lock.lock();
        try {
            while (number != 1) { // 多线程的判断使用while进行判断
                c1.await(); // 如果number不为1(说明不是它工作)让当前线程进行等待
            }
            // 打印5次
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " --> " + String.valueOf(i));
            }
            number = 2;// 修改number值为2
            c2.signal();// 通知线程2让他开始工作
        } finally {
            lock.unlock();
        }
    }

    public void print10() throws InterruptedException {
        lock.lock();
        try {
            while (number != 2) { // 如果number不为2则进行等待,否则执行后面的打印10次逻辑
                c2.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " -->" + String.valueOf(i));
            }
            number = 3; // 修改number的值为3
            c3.signal();// 通知3开始工作
        } finally {
            lock.unlock();
        }
    }

    public void print15() throws InterruptedException {
        lock.lock();
        try {
            while (number != 3) { // 如果number不为3则进行等待
                c3.await();
            }
            //打印15次
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + " --> " + String.valueOf(i));
            }
            number = 1;// 修改为1
            c1.signal();// 通知1工作
        } finally {
            lock.unlock();
        }
    }

}


public class Main {

    public static void main(String[] args) {

        PrintData printData = new PrintData();

        new Thread(() -> {
            for (int i = 0;i<10 ; i++) {// 只打印10次防止太多次了,若要一直循环则去掉i的条件判断
                try {
                    printData.print5();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i<10 ; i++) {
                try {
                    printData.print10();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0;i<10 ; i++) {
                try {
                    printData.print15();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

    }
}


运行结果

在这里插入图片描述


使用一个Condition完成线程调度

package cn.wideth.util;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class PrintData {

    private int number = 1;
    private ReentrantLock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();

    public void print5() throws InterruptedException {
        lock.lock();
        try {
            while (number != 1) { // 多线程的判断使用while进行判断
                c1.await(); // 如果number不为1(说明不是它工作)让当前线程进行等待
            }
            // 打印5次
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + String.valueOf(i));
            }
            number = 2;// 修改number值为2
            c1.signalAll();// 通知线程2让他开始工作
        } finally {
            lock.unlock();
        }
    }

    public void print10() throws InterruptedException {
        lock.lock();
        try {
            while (number != 2) { // 如果number不为2则进行等待,否则执行后面的打印10次逻辑
                c1.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + String.valueOf(i));
            }
            number = 3; // 修改number的值为3
            c1.signalAll();// 通知3开始工作
        } finally {
            lock.unlock();
        }
    }

    public void print15() throws InterruptedException {
        lock.lock();
        try {
            while (number != 3) { // 如果number不为3则进行等待
                c1.await();
            }
            //打印15次
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + String.valueOf(i));
            }
            number = 1;// 修改为1
            c1.signalAll();// 通知1工作
        } finally {
            lock.unlock();
        }
    }

}


public class Main {

    public static void main(String[] args) {

        PrintData printData = new PrintData();
        new Thread(() -> {
            for (int i = 0;i<10 ; i++) { // 只打印10次防止太多次了,若要一直循环则去掉i的条件判断
                try {
                    printData.print5();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i<10 ; i++) {
                try {
                    printData.print10();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0;i<10 ; i++) {
                try {
                    printData.print15();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

    }
}


源码分析

等待队列

要想能够深入的掌握condition还是应该知道它的实现原理,现在我们一起来看看condiiton的源码。创建一个condition对象是通过lock.newCondition(),而这个方法实际上是会new出一个ConditionObject对象,该类是AQS的一个内部类。前面我们说过,condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS,自然而然ConditionObject作为AQS的一个内部类无可厚非。我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列,同样的,condition内部也是使用同样的方式,内部维护了一个 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。另外注意到ConditionObject中有两个成员变量:

/**
     * Condition implementation for a {@link
     * AbstractQueuedSynchronizer} serving as the basis of a {@link
     * Lock} implementation.
     *
     * <p>Method documentation for this class describes mechanics,
     * not behavioral specifications from the point of view of Lock
     * and Condition users. Exported versions of this class will in
     * general need to be accompanied by documentation describing
     * condition semantics that rely on those of the associated
     * {@code AbstractQueuedSynchronizer}.
     *
     * <p>This class is Serializable, but all fields are transient,
     * so deserialized conditions have no waiters.
     */
    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;

        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }

这样我们就可以看出来ConditionObject通过持有等待队列的头尾指针来管理等待队列。主要注意的是Node类复用了在AQS中的Node类。


await实现原理

当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。接下来,我们还是从源码的角度去看,只有熟悉了源码的逻辑我们的理解才是最深的。await()方法源码为:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
	// 1. 将当前线程包装成Node,尾插入到等待队列中
    Node node = addConditionWaiter();
	// 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
		// 3. 当前线程进入到等待状态
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
	// 4. 自旋等待获取到同步状态(即获取到lock)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
	// 5. 处理被中断的情况
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}


signal实现原理

调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。我们来通过看源码的方式来看这样的猜想是不是对的,signal方法源码为:

public final void signal() {
    //1. 先检测当前线程是否已经获取lock
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
	Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}


本文小结

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。获取一个Condition必须通过Lock的newCondition()方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值