上篇我们讲了使用wait()和notify()使线程间实现合作,这种方式很直接也很灵活,但是使用之前需要获取对象的锁,notify()调用的次数如果小于等待线程的数量就会导致有的线程会一直等待下去。这篇我们讲多线程间接协作的方式,阻塞队列和管道通讯,间接协作的优点是使用起来更简单并且不易出错。

阻塞队列

阻塞队列提供了一种功能,即你可以在任何时刻向队列内扔一个对象,如果队列满了则当前线程阻塞;在任何时刻都可以从队列中取出一个对象,如果队列为空则当前线程阻塞。阻塞队列是线程安全的,使用它时无需加锁。此外其内部是使用显示锁实现的同步,使用Condition实现的线程阻塞。阻塞队列的接口是BlockingQueue,它有两个实现类:

1. ArrayBlockingQueue:底层使用数组实现的队列,有固定长度,调用其构造方法时必须提供队列的最大长度。

2. LinkedBlockingQueue:底层使用链表实现的队列,理论上讲是没有最大长度的,使用时不用提供队列长度;但实际上这个队列的长度不能超过Integer.MAX_VALUE。

这两个类使用的时候没有太大区别,我们以LinkedBlockingQueue为例,重写“学生去食堂打饭”的例子,代码如下:

class Student implements Runnable {
private Object wan = new Object();
public void run() {
try {
System.out.println("学生:取到了一个碗");
BlockingQueueTest.wanQueue.put(wan);
System.out.println("学生:阿姨帮忙盛饭");
wan = BlockingQueueTest.wanWithFanQueue.take();
System.out.println("学生:吃饭");
} catch (InterruptedException e) {}
}
}
class CafeteriaWorker implements Runnable {
public void run() {
try {
Object wan = BlockingQueueTest.wanQueue.take();
System.out.println("阿姨:给学生盛饭");
BlockingQueueTest.wanWithFanQueue.put(wan);
} catch (InterruptedException e) {}
}
}
public class BlockingQueueTest {
public static BlockingQueue wanQueue = new LinkedBlockingQueue();
public static BlockingQueue wanWithFanQueue = new LinkedBlockingQueue();
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Student());
exec.execute(new CafeteriaWorker());
exec.shutdown();
}
}

输出结果如下:

学生:取到了一个碗

学生:阿姨帮忙盛饭

阿姨:给学生盛饭

学生:吃饭

在这个例子中我们定义了两个队列,一个是空碗的队列,另一个是盛完饭的碗的队列。“学生线程”取到碗后将空碗放入wanQueue队列,然后试图从wanWithFanQueue队列中取出盛好的饭碗;“阿姨线程”试图从wanQueue队列中取出空碗,然后将盛好的饭碗放到wanWithFanQueue队列中。上次我们使用wait()方法时必须要求“阿姨线程”先启动,否则会导致“阿姨线程”错过学生的信号,而使用阻塞队列实现时我们就不再要求两个线程的启动顺序了,使用阻塞队列规避了错失信号的风险。有的同学可能会好奇为什么会使用两个队列,这是因为如果使用同一个队列,同学线程把碗扔进队列后,可能“阿姨线程”没来得及取出来就被“同学线程”拿回去了,感兴趣的同学可以自行测试。

管道通讯

通过管道的方式也可以使线程间实现交互,管道和阻塞队列类似,当管道内没有数据的时候,如果某个线程尝试去读取数据就会被阻塞。

我们可以使用PipedWriter和PipedReader来实现对管道数据的读取和写入。和阻塞队列不同的是,阻塞队列中不同线程都是操作一个队列的对象;使用管道时,不同的线程可以使用不同的对象,只要将它们注册为一个管道即可。

我们使用管道通信模拟一个线程对另一个线程表白,代码如下:

class Sender implements Runnable {
private PipedWriter writer;
Sender(PipedWriter writer) {
this.writer = writer;
}
public void run() {
String str1 = new String("I love you\n");
String str2 = new String("Do you love me\n");
try {
writer.write(str1.toCharArray());
writer.write(str2.toCharArray());
} catch (IOException e) {}
}
}
class Receiver implements Runnable {
private PipedReader reader;
public Receiver(PipedReader reader) {
this.reader = reader;
}
public void run() {
try {
while(true) {
char c = (char)reader.read();
System.out.print(c);
}
} catch (IOException e) {}
}
}
public class PipeCommunication {
public static void main(String[] args) throws Exception {
PipedReader reader = new PipedReader();
PipedWriter writer = new PipedWriter(reader);
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Sender(writer));
exec.execute(new Receiver(reader));
Thread.sleep(1000);
exec.shutdownNow();
}
}

运行后输出结果如下,一秒后程序退出:

I love you

Do you love me

我们在主方法里先定义了一个PipedReader对象,然后将这个对象作为PipedWriter的构造方法的参数传给PipedWriter对象,这样就实现两个输入输出流的绑定,分别将两个流对象传给两个线程对象。在信息的接收方我们使用一个死循环让其不断的从管道内读入,从输出结果可以看出read()方法在管道内没有数据的时候被阻塞了,因为输出结果没有循环打印其它字符。此外主线程sleep一秒后调用了shutdownNow()方法,这个方法向所有运行着的线程发送中断信号,程序运行一秒后就退出了,我们可以看出中断信号打断了Receiver的阻塞状态,由此得出结论:管道类阻塞时可以被中断信号打断。

总结

本篇讲了使用阻塞队列和管道来实现线程间的合作,相对于使用wait()协作而言这两种方式更为高级,使用起来更容易而且不易错,此外阻塞队列和管道都是线程安全的,因此使用它们的时候不需要使用锁。需要实现线程间协作时可以根据实际需要,权衡利弊进行选择。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。

Java并发编程(九)线程间协作(下)的更多相关文章

  1. Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  2. 19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  3. Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  4. 【转】Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

    一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线 ...

  5. Java多线程编程(6)--线程间通信(下)

      因为本文的内容大部分是以生产者/消费者模式来进行讲解和举例的,所以在开始学习本文介绍的几种线程间的通信方式之前,我们先来熟悉一下生产者/消费者模式.   在实际的软件开发过程中,经常会碰到如下场景 ...

  6. Java并发编程:线程控制

    在上一篇文章中(Java并发编程:线程的基本状态)我们介绍了线程状态的 5 种基本状态以及线程的声明周期.这篇文章将深入讲解Java如何对线程进行状态控制,比如:如何将一个线程从一个状态转到另一个状态 ...

  7. Java并发编程:线程池的使用

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

  8. Java并发编程:线程池的使用(转)

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

  9. Java并发编程:线程池的使用(转载)

    转载自:https://www.cnblogs.com/dolphin0520/p/3932921.html Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实 ...

  10. Java并发编程:线程池的使用(转载)

    文章出处:http://www.cnblogs.com/dolphin0520/p/3932921.html Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实 ...

随机推荐

  1. Ubuntu 17.04 upgrade to 17.10

    Just try  sudo do-release-upgrade if you get this An upgrade from 'zesty' to 'bionic' is not support ...

  2. Java 之常用API(一)

    常用API  1 API概述  2 Scanner类与String类  3 StringBuilder类 NO.one API概述 1.1 API概述 API(Application Programm ...

  3. poj 1475 推箱子

    bfs是一层层的遍历下去,每多一层即为多走一步,因此只要遇到T就停,此时肯定是最小步数. 所以这两层bfs应为,先对箱子的最少步数进行bfs,从而求出推箱子所用最少步数: 然后箱子bfs内部嵌入人的b ...

  4. webpack笔记三 管理输出

    webpack笔记三 管理输出 增加src/print.js: export default function printMe() { console.log('I get called from p ...

  5. switch与java

    switch结构可以更好的解决等值判断问题switch 选择结构的语法:switch (表达式){case 常量 1://代码块1:break;case 常量 2://代码块2:break;..... ...

  6. C# 特性 System.ComponentModel 命名空间属性方法大全,System.ComponentModel 命名空间的特性

    目录: System.ComponentModel 特性命名空间与常用类 System.ComponentModel.DataAnnotations ComponentModel - Classes ...

  7. 如何解决ORA-12547错误!

    最近在布置一个应用程序连接oracle后台数据库,数据库为oracle11g(11.2.0.2).应用程序属于root用户,所以需要以root用户通过系统认证的方式连接数据库.设置了root用户的环境 ...

  8. Python2.7 - IMOOC - 3

    第三章 Python变量和数据类型 3-4.变量 变量名必须是大小写英文.数字和下划线(_)的组合,且不能用数字开头. 同一个变量可以反复赋值,而且可以是不同类型的变量,例如: a = 123 # a ...

  9. ecshop hash登录 + wordpress mysql盲注字段

    delete_cart_goods.php  post  id=a * sq_xfkjbd 暴库and(select 1 from(select count(*),concat((select (se ...

  10. 记录智能指针使用shared_ptr使用错误

    shared_ptr为智能指针,今天一次在使用shared_ptr时,错误的将其初始化方式写为shared_ptr<T> test = shared_ptr<T>(),随后导致 ...