Java线程间通信的几种方式
Java编程思想中有这样一句话:
当我们使用线程来同时运行多个任务时,可以通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干扰到另外一个任务,这解决的是线程间彼此干涉的问题,现在我们需要来解决线程间彼此协调的问题,也就是线程间通信问题。
其实我一直对线程间通信这个概念比较模糊,如果仅仅从线程间相互协调来讲,只要不是单个线程孤立的独自执行,就都会涉及到线程间交互问题,都属于线程间通信的范畴。
- join:一个线程会让另外一个线程加入进来执行完成,这里就涉及到协调。
- 生产者/消费者模型:是生产者与消费者间的协调,两者间通过阻塞队列联系起来。
- 线程工具类,如CountDownLatch,Semaphore等也涉及到线程协调。
- 更广义一点的,线程中断,在一个线程中发起对另外一个线程的中断请求,另外一个线程响应中断,也涉及到协调。
以下是《Java编程思想》中提到的几种线程间通信的方式。
- wait/notify/notifyAll
- Lock和Condition
- 管道
- 阻塞队列
1.wait和notify/notifyAll
可以借助于Object类提供的wait()、notify()、notifyAll()三个方法,这三个方法属于Object类。但这三个方法必须由同步监视器对象来调用,这可分为两种情况
①对于用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
②对于用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。
需要注意的是notify()方法只能随机唤醒一个线程,如果要唤醒所有线程,请使用notifyAll()。
/** * 来源;《Java编程思想》P709 * 程序功能:一个餐厅有厨师和服务员。厨师准备好膳食后,通知服务员上菜,服务员上菜后等待。 * * 餐厅 */ public class Restaurant { Meal meal; ExecutorService exec = Executors.newCachedThreadPool(); WaitPerson waitPerson = new WaitPerson(this); Chef chef = new Chef(this); public Restaurant() { //启动厨师和服务员任务 exec.execute(chef); exec.execute(waitPerson); } public static void main(String[] args) { new Restaurant(); } } /** * 餐 */ class Meal { //订单号 private final int orderNum; public Meal(int orderNum) { this.orderNum = orderNum; } @Override public String toString() { return "Meal " + orderNum; } } /** * 服务员 */ class WaitPerson implements Runnable { private Restaurant restaurant; public WaitPerson(Restaurant r) { restaurant = r; } @Override public void run() { try { while (!Thread.interrupted()) { //服务员进入wait模式,直到被初始的notifyAll唤醒。 synchronized (this) {//A while (restaurant.meal == null) { // 等待厨师生产 wait(); } } System.out.println("服务员上餐 " + restaurant.meal); synchronized (restaurant.chef) {//B restaurant.meal = null; //在锁上调用notifyAll restaurant.chef.notifyAll(); // Ready for another } } } catch (InterruptedException e) { System.out.println("WaitPerson interrupted"); } } } /** * 厨师 */ class Chef implements Runnable { private Restaurant restaurant; private int count = 0; public Chef(Restaurant r) { restaurant = r; } @Override public void run() { try { while (!Thread.interrupted()) { //一直等待服务员收集到订单并通知厨师 synchronized (this) {//C while (restaurant.meal != null) { //等待服务员通知 wait(); } } //模拟食材用尽的情况 if (++count == 10) { System.out.println("食材用尽, 餐厅打烊"); restaurant.exec.shutdownNow(); } System.out.println("Order up! "); synchronized (restaurant.waitPerson) {//D //开始烹饪 restaurant.meal = new Meal(count); //在锁上调用notifyAll() //通知服务员 restaurant.waitPerson.notifyAll(); } TimeUnit.MILLISECONDS.sleep(100); } } catch (InterruptedException e) { System.out.println("Chef interrupted"); } } }
2.使用Lock和Condition
如果程序不用synchronized关键字来进行同步,而是用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。
使用Lock对象的方式,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
Condition将同步监视器方法wait()、notify()、notifyAll()分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法和同步代码块,Condition替代了同步监视器的功能。
Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法获得即可。Condition类提供了以下三个方法:
await():
signal():
signalAll():
3.使用阻塞队列控制线程通信
Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途不是作为容器,而是作为线程同步的工具。
BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满则线程被阻塞。而消费者线程 在取元素时,如果该队列已空则该线程被阻塞。
程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。
/** * 来源:《Java编程思想》P717的练习 * 程序目的:用阻塞队列改写管道流的例子 * 程序功能:写线程向阻塞队列添加字符,读线程从阻塞队列获取字符并打印到控制台,读取不到时阻塞。 */ public class SendReceiveBQ { public static void main(String[] args) throws InterruptedException, IOException { Sender sender = new Sender(); Receiver receiver = new Receiver(sender); ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(sender); exec.execute(receiver); TimeUnit.SECONDS.sleep(4); exec.shutdownNow(); } } class CharQueue extends LinkedBlockingQueue<Character> { } class Receiver implements Runnable { private CharQueue in; Receiver(Sender sender) { //使用同一个阻塞队列 in = sender.getQueue(); } @Override public void run() { try { while (true) { //阻塞,直到读取到字符 System.out.println("Receiver:" + in.take()); } } catch (InterruptedException e) { System.out.println(e + " Receiver interrupted"); } } } class Sender implements Runnable { private Random random = new Random(47); private CharQueue out = new CharQueue(); public CharQueue getQueue() { return out; } @Override public void run() { try { while (true) { for (char c = 'A'; c <= 'z'; c++) { out.put(c); TimeUnit.MILLISECONDS.sleep(random.nextInt(500)); } } } catch (InterruptedException e) { System.out.println(e + " Sender sleep interrupted"); } } }
4.使用管道流进行线程通信(被阻塞队列替代)
管道通信模型可以看成是生产者-消费者模型的变种,管道就是一个封装好的解决方案。管道基本上就是一个阻塞队列,所以可以用于线程间通信。管道在Java中对应的实现是PipedWriter类和PipedReader类。
但随着阻塞队列的出现,管道逐渐被替代。所以,在实际开发中,很少会使用到管道流。
下面是Java编程思想给出的管道应用的例子。了解更多:java管道流 PipedInputStream & PipedOutputStream 的应用。
/** * 来源:《Java编程思想》P717 * 程序目的:学习管道流的使用 * 程序功能:写线程向管道写入字符,读线程从管道获取字符并打印到控制台,读取不到时阻塞。 */ public class PipedIO { public static void main(String[] args) throws InterruptedException, IOException { Sender sender = new Sender(); Receiver receiver = new Receiver(sender); ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(sender); exec.execute(receiver); TimeUnit.SECONDS.sleep(4); exec.shutdownNow(); } } class Receiver implements Runnable { private PipedReader in; Receiver(Sender sender) throws IOException { // PipedReader的创建需要与PipedWriter相连 // 方式一:创建时就关联 in = new PipedReader(sender.getPipedWriter()); // 方式二:先创建,再关联 // in = new PipedReader(); // in.connect(sender.getPipedWriter()); } @Override public void run() { try { while (true) { //阻塞,直到读取到字符 System.out.println("Receiver:" + (char) in.read()); } } catch (IOException e) { e.printStackTrace(); } } } class Sender implements Runnable { private Random random = new Random(47); private PipedWriter out = new PipedWriter(); /** * Reader中的PipedReader需要与Sender中的PipedWriter相连,所以这里暴露方法 */ public PipedWriter getPipedWriter() { return out; } @Override public void run() { try { //循环 while (true) { //输出A-z之间的字符 for (char c = 'A'; c <= 'z'; c++) { out.write(c); //随机睡眠,让读线程有时间运行 TimeUnit.MILLISECONDS.sleep(random.nextInt(500)); } } } catch (IOException e) { System.out.println(e + " Sender write exception"); } catch (InterruptedException e) { System.out.println(e + " Sender sleep interrupted"); } } }