Java编程的逻辑 (72) - 显式条件
本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http://item.jd.com/12299018.html

上节我们介绍了显式锁,本节介绍关联的显式条件,介绍其用法和原理。显式条件也可以被称做条件变量、条件队列、或条件,后文我们可能会交替使用。
用法
基本概念和方法
锁用于解决竞态条件问题,条件是线程间的协作机制。显式锁与synchronzied相对应,而显式条件与wait/notify相对应。wait/notify与synchronized配合使用,显式条件与显式锁配合使用。
条件与锁相关联,创建条件变量需要通过显式锁,Lock接口定义了创建方法:
Condition newCondition();
Condition表示条件变量,是一个接口,它的定义为:
public interface Condition {
  void await() throws InterruptedException;
  void awaitUninterruptibly();
  long awaitNanos(long nanosTimeout) throws InterruptedException;
  boolean await(long time, TimeUnit unit) throws InterruptedException;
  boolean awaitUntil(Date deadline) throws InterruptedException;
  void signal();
  void signalAll();
}
await()对应于Object的wait(),signal()对应于notify,signalAll()对应于notifyAll(),语义也是一样的。
与Object的wait方法类似,await也有几个限定等待时间的方法,但功能更多一些:
//等待时间是相对时间,如果由于等待超时返回,返回值为false,否则为true
boolean await(long time, TimeUnit unit) throws InterruptedException;
//等待时间也是相对时间,但参数单位是纳秒,返回值是nanosTimeout减去实际等待的时间
long awaitNanos(long nanosTimeout) throws InterruptedException;
//等待时间是绝对时间,如果由于等待超时返回,返回值为false,否则为true
boolean awaitUntil(Date deadline) throws InterruptedException;
这些await方法都是响应中断的,如果发生了中断,会抛出InterruptedException,但中断标志位会被清空。Condition还定义了一个不响应中断的等待方法:
void awaitUninterruptibly();
该方法不会由于中断结束,但当它返回时,如果等待过程中发生了中断,中断标志位会被设置。
一般而言,与Object的wait方法一样,调用await方法前需要先获取锁,如果没有锁,会抛出异常IllegalMonitorStateException。await在进入等待队列后,会释放锁,释放CPU,当其他线程将它唤醒后,或等待超时后,或发生中断异常后,它都需要重新获取锁,获取锁后,才会从await方法中退出。
另外,与Object的wait方法一样,await返回后,不代表其等待的条件就一定满足了,通常要将await的调用放到一个循环内,只有条件满足后才退出。
一般而言,signal/signalAll与notify/notifyAll一样,调用它们需要先获取锁,如果没有锁,会抛出异常IllegalMonitorStateException。signal与notify一样,挑选一个线程进行唤醒,signalAll与notifyAll一样,唤醒所有等待的线程,但这些线程被唤醒后都需要重新竞争锁,获取锁后才会从await调用中返回。
用法示例
ReentrantLock实现了newCondition方法,通过它,我们来看下条件的基本用法。我们实现与67节类似的例子WaitThread,一个线程启动后,在执行一项操作前,等待主线程给它指令,收到指令后才执行,示例代码为:
public class WaitThread extends Thread {
    private volatile boolean fire = false;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    @Override
    public void run() {
        try {
            lock.lock();
            try {
                while (!fire) {
                    condition.await();
                }
            } finally {
                lock.unlock();
            }
            System.out.println("fired");
        } catch (InterruptedException e) {
            Thread.interrupted();
        }
    }
    public void fire() {
        lock.lock();
        try {
            this.fire = true;
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        WaitThread waitThread = new WaitThread();
        waitThread.start();
        Thread.sleep(1000);
        System.out.println("fire");
        waitThread.fire();
    }
}
需要特别注意的是,不要将signal/signalAll与notify/notifyAll混淆,notify/notifyAll是Object中定义的方法,Condition对象也有,稍不注意就会误用,比如,对上面例子中的fire方法,可能会写为:
public void fire() {
    lock.lock();
    try {
        this.fire = true;
        condition.notify();
    } finally {
        lock.unlock();
    }
}
写成这样,编译器不会报错,但运行时会抛出IllegalMonitorStateException,因为notify的调用不在synchronized语句内。
同样,避免将锁与synchronzied混用,那样非常令人混淆,比如:
public void fire() {
    synchronized(lock){
        this.fire = true;
        condition.signal();
    }
}
记住,显式条件与显式锁配合,wait/notify与synchronized配合。
生产者/消费者模式
在67节,我们用wait/notify实现了生产者/消费者模式,我们提到了wait/notify的一个局限,它只能有一个条件等待队列,分析等待条件也很复杂。在生产者/消费者模式中,其实有两个条件,一个与队列满有关,一个与队列空有关。使用显式锁,可以创建多个条件等待队列。下面,我们用显式锁/条件重新实现下其中的阻塞队列,代码为:
static class MyBlockingQueue<E> {
    private Queue<E> queue = null;
    private int limit;
    private Lock lock = new ReentrantLock();
    private Condition notFull  = lock.newCondition();
    private Condition notEmpty = lock.newCondition();
    public MyBlockingQueue(int limit) {
        this.limit = limit;
        queue = new ArrayDeque<>(limit);
    }
    public void put(E e) throws InterruptedException {
        lock.lockInterruptibly();
        try{
            while (queue.size() == limit) {
                notFull.await();
            }
            queue.add(e);
            notEmpty.signal();
        }finally{
            lock.unlock();
        }
    }
    public E take() throws InterruptedException {
        lock.lockInterruptibly();
        try{
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            E e = queue.poll();
            notFull.signal();
            return e;
        }finally{
            lock.unlock();
        }
    }
}
定义了两个等待条件:不满(notFull)、不空(notEmpty),在put方法中,如果队列满,则在noFull上等待,在take方法中,如果队列空,则在notEmpty上等待,put操作后通知notEmpty,take操作后通知notFull。
这样,代码更为清晰易读,同时避免了不必要的唤醒和检查,提高了效率。Java并发包中的类ArrayBlockingQueue就采用了类似的方式实现。
实现原理
ConditionObject
理解了显式条件的概念和用法,我们来看下ReentrantLock是如何实现它的,其newCondition()的代码为:
public Condition newCondition() {
    return sync.newCondition();
}
sync是ReentrantLock的内部类对象,其newCondition()代码为:
final ConditionObject newCondition() {
    return new ConditionObject();
}
ConditionObject是AQS中定义的一个内部类,不了解AQS请参看上节。ConditionObject的实现也比较复杂,我们通过一些主要代码来简要探讨其实现原理。ConditionObject内部也有一个队列,表示条件等待队列,其成员声明为:
//条件队列的头节点
private transient Node firstWaiter;
//条件队列的尾节点
private transient Node lastWaiter;
ConditionObject是AQS的成员内部类,它可以直接访问AQS中的数据,比如AQS中定义的锁等待队列。
我们看下几个方法的实现,先看await方法。
await实现分析
下面是await方法的代码,我们通过添加注释解释其基本思路。
public final void await() throws InterruptedException {
    // 如果等待前中断标志位已被设置,直接抛异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 1.为当前线程创建节点,加入条件等待队列
    Node node = addConditionWaiter();
    // 2.释放持有的锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 3.放弃CPU,进行等待,直到被中断或isOnSyncQueue变为true
    // isOnSyncQueue为true表示节点被其他线程从条件等待队列
    // 移到了外部的锁等待队列,等待的条件已满足
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 4.重新获取锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 5.处理中断,抛出异常或设置中断标志位
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
awaitNanos实现分析
awaitNanos与await的实现是基本类似的,区别主要是会限定等待的时间,如下所示:
public final long awaitNanos(long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    long lastTime = System.nanoTime();
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (nanosTimeout <= 0L) {
            //等待超时,将节点从条件等待队列移到外部的锁等待队列
            transferAfterCancelledWait(node);
            break;
        }
        //限定等待的最长时间
        LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        long now = System.nanoTime();
        //计算下次等待的最长时间
        nanosTimeout -= now - lastTime;
        lastTime = now;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return nanosTimeout - (System.nanoTime() - lastTime);
}
signal实现分析
signal方法代码为:
public final void signal() {
    //验证当前线程持有锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //调用doSignal唤醒等待队列中第一个线程
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}
doSignal的代码就不列举了,其基本逻辑是:
- 将节点从条件等待队列移到锁等待队列
 - 调用LockSupport.unpark将线程唤醒
 
小结
本节介绍了显式条件的用法和实现原理。它与显式锁配合使用,与wait/notify相比,可以支持多个条件队列,代码更为易读,效率更高,使用时注意不要将signal/signalAll误写为notify/notifyAll。
从70节到本节,我们介绍了Java并发包的基础 - 原子变量和CAS、显式锁和条件,基于这些,Java并发包还提供了很多更为易用的高层数据结构、工具和服务,从下一节开始,我们先探讨一些并发数据结构。
(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic)
----------------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (72) - 显式条件的更多相关文章
- Java编程的逻辑 (71) - 显式锁
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - 《Java编程的逻辑》 - 文章列表
		
<计算机程序的思维逻辑>系列文章已整理成书<Java编程的逻辑>,由机械工业出版社出版,2018年1月上市,各大网店有售,敬请关注! 京东自营链接:https://item.j ...
 - Java编程的逻辑 (83) - 并发总结
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - Java编程的逻辑 (9) - 条件执行的本质
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
 - Java编程的逻辑 (81) - 并发同步协作工具
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - Java编程的逻辑 (76) - 并发容器 - 各种队列
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - Java编程的逻辑 (95) - Java 8的日期和时间API
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - Java编程的逻辑 (92) - 函数式数据处理 (上)
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
 - Java编程的逻辑 (65) - 线程的基本概念
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 
随机推荐
- Java 多线程初级汇总
			
多线程概述 抢占式多任务 直接中断而不需要事先和被中断程序协商 协作多任务 被中断程序同意交出控制权之后才能执行中断 多线程和多进程区别? 本质的区别在于每个进程有它自己的变量的完备集,线程则共享相同 ...
 - Installshield 打包安装程序时写入注册表,及运行bat文件
			
一.写入注册表 1. 打开project assistant –> Project Registry 可以像注册表里一样操作,其中[INSTALLDIR]是指的安装路径 二. 运行bat文件 ...
 - alpine编译安装tengine,并使用supervisor启动
			
Alpine是一个小型的linux系统,官方docker镜像只有不到5MB,非常适合作为容器镜像. Alpine Linux is a security-oriented, lightweight L ...
 - 敏捷持续集成(Jenkins)
			
在前面已经完成git和gitlab的相关操作 1.持续集成的概念: 1. 什么是持续集成: 持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可 ...
 - webpack进阶--打包
			
上一片博文主要让大家了解下究竟webpack是干什么的,明显它是专注于打包的. gulp 和 webpack 的区别 gulp,要求我们一步步写task(es6编译.css压缩.图片压缩.打包. ...
 - sql查询结果存入DataTable,然后从DataTable取数据
			
public static DataTable SqlConnectionInformation() { string connstr = ConfigurationManager.Connectio ...
 - JVM总结(一):概述--JVM对象探秘
			
这一节我们来讨论一下JVM对象建立过程. JVM对象探秘 对象的建立 对象的内存布局 对象的访问定位 JVM对象探秘 对象的建立 对象的建立过程 图一:对象建立过程 1.类加载检查. 当JVM检测 ...
 - CSS Counter Style试玩儿
			
2015年2月3日,CSS Counter Style level3成为了W3C的候选标准,是时候来一探究竟,看看强大魔力的@counter-style如何自定义list-style和counter. ...
 - ThinkPHP框架学习(一)
			
这几天呢,断断续续地在看孙叔华老师的ThinkPHP教程,期间还做了一些其他事情,出去办了点事,总结总结下一学期规划等等,不知不觉间又过去了大半个星期.现在呢,看完了一天的教程,在这里,还是希望稍微总 ...
 - BAT及各大互联网公司2014前端笔试面试题--JavaScript篇(昨天某个群友表示写的简单了点,然后我无情的把他的抄了一遍)
			
(某个群友)http://www.cnblogs.com/coco1s/ 很多面试题是我自己面试BAT亲身经历碰到的.整理分享出来希望更多的前端er共同进步吧,不仅适用于求职者,对于巩固复习js更是大 ...