Thinking in java 之并发其四:线程之间的协作(上)
一、前言
在第二章的时候,我们学会了通过锁的方式来同步多个任务,从而使得一个任务不会干涉另一个任务的资源。也就是说,多个任务在交替步入某项共享资源(通常是内存),可以使用互斥来使得任何时刻只有一个任务可以访问这项资源。而接下来,我们需要学习如何使任务彼此之间可以协作,以使得多个任务可以一起工作去解决问题。那么,我们面临的问题不再是彼此之间的干涉,而是彼此之间的协调。
当任务协作时,关键问题是这些任务之间的握手。为了实现这种握手,我们使用了相同的基础特性:互斥。在这种情况下,互斥能够确保只有一个任务可以相应某个信号,这样就可以根除任何可能的竞争条件。在互斥之上,我们为任务添加了一种途径,可以将其自身挂起,直到知道某些外部条件发生变化,表示现在可以让这个任务向前开动了为止。
二、wait() 和 notifyAll()
wait() 可以使任务挂起,直到 notify() 或者 notifyAll() 将其唤起。通常我们在任务需要等待前置任务完成时将其挂起,当前置任务完成时在将其唤醒以继续执行。
在使用 sleep 或者 yeild 的时候,对象的锁并没有被释放,也就是说,其他任务还是不能够访问被锁的资源。而使用 wait() 时,将会释放锁,这意味着另一个任务可以获得这个锁,并且可以执行其他的 synchronized 方法。
wait() 也可以使用时间作为参数,表示,当指定的时间长度过去之后,任务会自动唤醒。当然,在时间到达之前,我们也可以通过 notify() 或者 notifyAll() 将其唤醒。并且在挂起的这段时间里,锁是释放的。
与 sleep() 等方法不同的是,wait()、notify() 和 notifyAll() 不属于 Thread 而是属于 Object。所以,即便是在非同控制的方法里,我们依旧可以调用这三个方法,但是会抛出异常。
现在通过一个简单的示例,来展示任务之间是如何相互配合的。WaxOMatic.java 有两个过程,一个是将蜡涂到 Car 上,一个是抛光它。抛光任务在涂蜡完成之前,是不能执行的。而涂蜡在涂另一层蜡之前,必须等待抛光任务完成。WaxOn 和 WaxOff 都使用了 Car 对象,该对象在这些任务等待条件变化的时候,使用 wait() 和 notufyAll() 来挂起和重启。
1 package ThreadTest.cooperation; 2 3 import java.util.concurrent.ExecutorService; 4 import java.util.concurrent.Executors; 5 import java.util.concurrent.TimeUnit; 6 7 class Car{ 8 private boolean waxOn = false; 9 public synchronized void waxed() { 10 waxOn =true; 11 notifyAll(); 12 } 13 public synchronized void buffed() { 14 waxOn = false; 15 notifyAll(); 16 } 17 18 public synchronized void waitForWaxing() throws InterruptedException { 19 while(waxOn == false) 20 wait(); 21 } 22 23 public synchronized void waitFotBuffing() throws InterruptedException { 24 while(waxOn == true) 25 wait(); 26 } 27 } 28 29 class WaxOn implements Runnable{ 30 private Car car; 31 public WaxOn(Car car) { 32 this.car = car; 33 } 34 public void run() { 35 try { 36 while(!Thread.interrupted()) { 37 System.out.println("Wax On!"); 38 TimeUnit.MILLISECONDS.sleep(200); 39 car.waxed(); 40 car.waitFotBuffing(); 41 } 42 }catch(InterruptedException e) { 43 System.out.println("Exit via interrupt!"); 44 } 45 System.out.println("Ending Wax On task"); 46 } 47 } 48 49 class WaxOff implements Runnable{ 50 private Car car; 51 public WaxOff(Car car) { 52 this.car=car; 53 } 54 @Override 55 public void run() { 56 try { 57 while(!Thread.interrupted()) { 58 car.waitForWaxing(); 59 System.out.println("Wax Off!"); 60 TimeUnit.MILLISECONDS.sleep(200); 61 car.buffed(); 62 } 63 64 } catch (InterruptedException e) { 65 // TODO Auto-generated catch block 66 System.out.println("Exit via interrupt!"); 67 } 68 System.out.println("Ending Wax On task"); 69 70 } 71 72 } 73 74 public class WaxOMatic { 75 76 public static void main(String[] args) throws InterruptedException { 77 Car car = new Car(); 78 ExecutorService exec = Executors.newCachedThreadPool(); 79 exec.execute(new WaxOff(car)); 80 exec.execute(new WaxOn(car)); 81 TimeUnit.SECONDS.sleep(5); 82 exec.shutdownNow(); 83 84 } 85 86 }
car 的 WaxOn 初始状态为 false 即 未涂蜡,这个时候,buff 是不能执行的,而在涂蜡之后,buff 可以执行,但 waxed 不能执行。WaxForWaxing 会循环检测 WaxOn 的状态,当其为false 时,唤醒所有任务,由于此时 WaxOn 为 false 所以只能执行 waxed 。同理 WaxForWaxing 也可以唤醒 Buff。
这里我们使用了 exec.shutdownNow() 他和 exec.shutdown() 的区别是,后者不会中断当前正在执行的任务,而前者会中断当前的任务。当我们调用 exec.shutdownNow() 时,他会立刻执行所有 Thread (由它发起的) 的 Inturrept();
三、notify() 和 notifyAll()
notify() 和 notifyall() 的区别似乎在于唤醒一个阻塞任务,还是所有阻塞任务。而我们必须要思考的问题是,当做个任务处于阻塞状态是,notify() 唤醒的是哪一个?是随机唤醒?或者一其他方式唤醒任务。而对于 noifyAll() 是指唤醒所有位置的任务吗?先看下面这个示例:
1 package ThreadTest.cooperation; 2 3 import java.util.Timer; 4 import java.util.TimerTask; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.TimeUnit; 8 9 class Blocker{ 10 synchronized void waitingCall() { 11 try { 12 //只要程序没中断,会不断的执行下面的代码, 13 while(!Thread.interrupted()) { 14 //阻塞任务 15 wait(); 16 System.out.println(Thread.currentThread() + " "); 17 } 18 //System.out.print("\n"); 19 }catch(InterruptedException e) { 20 System.out.println("Exiting"); 21 } 22 } 23 synchronized void prod() {notify();} 24 synchronized void prodAll() {notifyAll();} 25 } 26 27 class Task implements Runnable{ 28 static Blocker blocker = new Blocker(); 29 30 @Override 31 public void run() { 32 blocker.waitingCall(); 33 } 34 } 35 36 class Task2 implements Runnable{ 37 static Blocker blocker = new Blocker(); 38 public void run() { 39 blocker.waitingCall(); 40 } 41 } 42 public class NotifyVSNotifyAll { 43 44 public static void main(String[] args) throws InterruptedException { 45 ExecutorService exec = Executors.newCachedThreadPool(); 46 for(int i=0;i<5;i++) { 47 exec.execute(new Task()); 48 } 49 exec.execute(new Task2()); 50 Timer timer = new Timer(); 51 timer.scheduleAtFixedRate(new TimerTask() { 52 boolean prod = true; 53 public void run() { 54 if(prod) { 55 System.out.println("notify() "); 56 Task.blocker.prod(); 57 prod = false; 58 }else { 59 System.out.println("notifyAll() "); 60 Task.blocker.prodAll(); 61 prod = true; 62 } 63 } 64 },400,400); 65 TimeUnit.SECONDS.sleep(4); 66 timer.cancel(); 67 System.out.println("Timer canceled"); 68 TimeUnit.MILLISECONDS.sleep(500); 69 System.out.println("shuting down"); 70 exec.shutdown(); 71 } 72 73 } 74 75 76 //output 77 /*notify() 78 Thread[pool-1-thread-1,5,main] 79 notifyAll() 80 Thread[pool-1-thread-2,5,main] 81 Thread[pool-1-thread-1,5,main] 82 Thread[pool-1-thread-5,5,main] 83 Thread[pool-1-thread-4,5,main] 84 Thread[pool-1-thread-3,5,main] 85 notify() 86 Thread[pool-1-thread-2,5,main] 87 notifyAll() 88 Thread[pool-1-thread-1,5,main] 89 Thread[pool-1-thread-2,5,main] 90 Thread[pool-1-thread-3,5,main] 91 Thread[pool-1-thread-4,5,main] 92 Thread[pool-1-thread-5,5,main] 93 notify() 94 Thread[pool-1-thread-1,5,main] 95 notifyAll() 96 Thread[pool-1-thread-2,5,main] 97 Thread[pool-1-thread-1,5,main] 98 Thread[pool-1-thread-5,5,main] 99 Thread[pool-1-thread-4,5,main] 100 Thread[pool-1-thread-3,5,main] 101 notify() 102 Thread[pool-1-thread-2,5,main] 103 notifyAll() 104 Thread[pool-1-thread-1,5,main] 105 Thread[pool-1-thread-2,5,main] 106 Thread[pool-1-thread-3,5,main] 107 Thread[pool-1-thread-4,5,main] 108 Thread[pool-1-thread-5,5,main] 109 notify() 110 Thread[pool-1-thread-1,5,main] 111 notifyAll() 112 Timer canceled 113 Thread[pool-1-thread-2,5,main] 114 Thread[pool-1-thread-1,5,main] 115 Thread[pool-1-thread-5,5,main] 116 Thread[pool-1-thread-4,5,main] 117 Thread[pool-1-thread-3,5,main] 118 shuting down 119 */
从输出结果来看,有以下结论:
- 当任务因 wait() 被阻塞时,会释放锁。waitingCall() 被阻塞之后,我们可以使用和它一起被 synchronized 修饰的 prod() 和 prodAll() 方法充分的说明了这点。
- notifyAll() 只能唤醒和它拥有同一个锁的任务,比如我们再调用了 Task.prodAll() 之后,Task2的任务并没有被唤醒。那么,不经产生疑问,如果调用 notifyAll() 的方法不被 synchronized 修饰,会怎么样。答案是会抛出异常,不仅如此,wait(),notify() 也必须被 synchronized 修饰。否则,java 会抛出 java.lang.IllegalMonitorStateException 异常。
- 对于 notify() 到底唤醒了哪个对象,从结果来看,似乎总是唤醒了第一个线程,也就是最先启动的线程。在 java 对于 notify() 的解释中,可以发现,它是随机的。
Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object's monitor by calling one of the
wait
methods.
以上是 java 对于 notify() 的注释。
四、经典协作问题
关于线程之间的写作,有很多经典的例题,其中比较被人熟知的事生产者和消费者问题:
在一个饭店,它有一个厨师和一个服务员。服务员必须等待厨师准备好食物。当厨师准备好后,他会通知服务员,之后服务员上菜,然后返回继续等待。这是一个简单的任务协作示例。厨师代表生产者,服务员代表消费者。两个任务必须在食物被生产和消费时进行握手,而系统必须以有序的方式关闭。以下是对这个叙述建模的代码。
1 package ThreadTest.cooperation; 2 3 import java.util.concurrent.ExecutorService; 4 import java.util.concurrent.Executors; 5 import java.util.concurrent.TimeUnit; 6 7 class Meal{ 8 private final int orderNum; 9 public Meal(int orderNum) { 10 this.orderNum = orderNum; 11 } 12 public String toString() { 13 return "Meal " + orderNum; 14 } 15 } 16 17 class WaitPerson implements Runnable{ 18 19 private Restaurant restaurant; 20 public WaitPerson(Restaurant restaurant) { 21 this.restaurant = restaurant; 22 } 23 @Override 24 public void run() { 25 try { 26 while(!Thread.interrupted()) { 27 //如果肉是空的,就一直等待 28 synchronized(this) { 29 while(restaurant.meal == null) { 30 System.out.println("no meal wait"); 31 wait(); 32 } 33 } 34 System.out.println("Waitperson got" + restaurant.meal); 35 synchronized(restaurant.chef) { 36 //该锁是针对厨师的,告诉厨师没肉了,唤醒厨师生产肉的任务 37 restaurant.meal = null; 38 restaurant.chef.notifyAll(); 39 } 40 } 41 42 }catch(InterruptedException e) { 43 System.out.println("WaitPersion interrupted"); 44 } 45 } 46 47 } 48 49 class Chef implements Runnable{ 50 51 private Restaurant restaurant; 52 private int count = 0; 53 public Chef(Restaurant restaurant) { 54 this.restaurant = restaurant; 55 } 56 @Override 57 public void run() { 58 try { 59 while(!Thread.interrupted()) { 60 synchronized(this) { 61 while(restaurant.meal !=null) { 62 wait(); 63 } 64 } 65 if(++count== 10) { 66 System.out.println("Out of food, close"); 67 restaurant.exec.shutdownNow(); 68 } 69 System.out.println("Order Up"); 70 synchronized(restaurant.waitPerson) { 71 System.out.println("Meal count: " + count); 72 restaurant.meal=new Meal(count); 73 restaurant.waitPerson.notifyAll(); 74 } 75 TimeUnit.MILLISECONDS.sleep(100); 76 } 77 }catch(InterruptedException e) { 78 System.out.println("chef interrupt"); 79 } 80 } 81 82 } 83 public class Restaurant { 84 Meal meal; 85 Chef chef = new Chef(this); 86 WaitPerson waitPerson = new WaitPerson(this); 87 ExecutorService exec = Executors.newCachedThreadPool(); 88 public Restaurant() { 89 exec.execute(chef); 90 exec.execute(waitPerson); 91 } 92 public static void main(String[] args) { 93 new Restaurant(); 94 95 } 96 97 } 98 //Output 99 /*Order Up 100 no meal wait 101 Meal count: 1 102 Waitperson gotMeal 1 103 no meal wait 104 Order Up 105 Meal count: 2 106 Waitperson gotMeal 2 107 no meal wait 108 Order Up 109 Meal count: 3 110 Waitperson gotMeal 3 111 no meal wait 112 Order Up 113 Meal count: 4 114 Waitperson gotMeal 4 115 no meal wait 116 Order Up 117 Meal count: 5 118 Waitperson gotMeal 5 119 no meal wait 120 Order Up 121 Meal count: 6 122 Waitperson gotMeal 6 123 no meal wait 124 Order Up 125 Meal count: 7 126 Waitperson gotMeal 7 127 no meal wait 128 Order Up 129 Meal count: 8 130 Waitperson gotMeal 8 131 no meal wait 132 Order Up 133 Meal count: 9 134 Waitperson gotMeal 9 135 no meal wait 136 Out of food, close 137 Order Up 138 WaitPersion interrupted 139 Meal count: 10 140 chef interrupt*/
这是一个很简单的生产者与消费者的示例。在示例中,chef 和 waitPerson 都和同一个 restaurant 绑定。并通过这个 restaurant 来找到彼此(唤醒彼此的任务)。另外需要注意的点事是 synchronized 的使用。在前面的章节,我们说到过当 synchronized() 括号内指定的对象不同时,它们时属于不同的锁。而 notifyAll(或者是 notify()) 只能唤醒和自己共享同一把锁的代码块。因此 chef 生产肉的代码的 sychronized()括号里是对应的 waitPerson。只有这样才能唤醒 waitPerson 里 synchronized(this)的代码。同理,waitPerson 的拿走肉的代码,也应该和唤醒 chef 的代码有对应的锁。
也许你会注意到 当我们通过 “restaurant.exec.shutdownNow();” 去结束这个任务之后,输出里依旧出现了 " Order Up " 这说明 shutdownNow() 并没有立刻将任务停止。而 shutdownNow() 是立即调用 pool 里所有的 Thread 的 interrupt() 方法,但是对于一个在阻塞状态下的任务,interrupt() 的调用会抛出 InterruptedException。