五、使用显示的 Lock 和 Condition 对象
在 JUC(java.util.concurrent) 中,还有额外的显示工具可以用来重写WaxOMatic.java。使用互斥并允许任务挂起的基本类型是 Condition,可以通过在 Condition 上调用 await() 来挂起一个任务。当外部条件发生变化,意味着某个任务应该继续执行时,你可以通过调用 signal() 来通知这个任务,从而唤醒一个任务,或者调用 signalAll() 来唤醒所有在这个 Condition 上被其自身挂起的任务(与使用 notify() 相比,signalAll() 是更安全的方式)。
下面则是WaxOMatic.java 的重写版本:
1 package ThreadTest.cooperation; 2 3 import java.util.concurrent.ExecutorService; 4 import java.util.concurrent.Executors; 5 import java.util.concurrent.TimeUnit; 6 import java.util.concurrent.locks.Condition; 7 import java.util.concurrent.locks.Lock; 8 import java.util.concurrent.locks.ReentrantLock; 9 10 class Car{ 11 private Lock lock = new ReentrantLock(); 12 private Condition condition = lock.newCondition(); 13 private boolean waxOn = false; 14 public void waxed() { 15 lock.lock(); 16 try { 17 waxOn =true; 18 condition.signalAll();; 19 }finally { 20 lock.unlock(); 21 } 22 23 } 24 public void buffed() { 25 lock.lock(); 26 try { 27 waxOn = false; 28 condition.signal(); 29 }finally { 30 lock.unlock(); 31 } 32 33 } 34 35 public void waitForWaxing() throws InterruptedException { 36 lock.lock(); 37 try { 38 while(waxOn == false) 39 condition.await(); 40 }finally { 41 lock.unlock(); 42 } 43 44 } 45 46 public void waitFotBuffing() throws InterruptedException { 47 lock.lock(); 48 try { 49 while(waxOn == true) 50 condition.await(); 51 }finally { 52 lock.unlock(); 53 } 54 } 55 } 56 57 class WaxOn implements Runnable{ 58 private Car car; 59 public WaxOn(Car car) { 60 this.car = car; 61 } 62 public void run() { 63 try { 64 while(!Thread.interrupted()) { 65 System.out.println("Wax On!"); 66 TimeUnit.MILLISECONDS.sleep(200); 67 car.waxed(); 68 car.waitFotBuffing(); 69 } 70 }catch(InterruptedException e) { 71 System.out.println("Exit via interrupt!"); 72 } 73 System.out.println("Ending Wax On task"); 74 } 75 } 76 77 class WaxOff implements Runnable{ 78 private Car car; 79 public WaxOff(Car car) { 80 this.car=car; 81 } 82 @Override 83 public void run() { 84 try { 85 while(!Thread.interrupted()) { 86 car.waitForWaxing(); 87 System.out.println("Wax Off!"); 88 TimeUnit.MILLISECONDS.sleep(200); 89 car.buffed(); 90 } 91 92 } catch (InterruptedException e) { 93 // TODO Auto-generated catch block 94 System.out.println("Exit via interrupt!"); 95 } 96 System.out.println("Ending Wax On task"); 97 98 } 99 100 } 101 102 public class WaxOMatic { 103 104 public static void main(String[] args) throws InterruptedException { 105 Car car = new Car(); 106 ExecutorService exec = Executors.newCachedThreadPool(); 107 exec.execute(new WaxOff(car)); 108 exec.execute(new WaxOn(car)); 109 TimeUnit.SECONDS.sleep(5); 110 exec.shutdownNow(); 111 112 } 113 114 }
与原来的代码对比来看,新的 WaxOMatic 使用了 Lock 来代替 synchronized(前面有提到 lock 的用法)。同时用 condition 的 signal() 和 await() 来代替 wait() 和 notify()。notify() 只能幻唤醒和自己共享锁的代码块,同样的,condition 的 signal() 也只能唤醒同一个 Lock 的 任务。
六、生产者——消费者与队列
在上一篇里,我们用 wait() 和 notifyAll() 完成了生产者——消费者的任务协作问题,即每次交互时都握手(notifyAll() 唤醒对方)。还可以使用更高的抽象级别,使用同步队列来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。 在 JUC 的 BlockingQueue 中提供了这个队列,这个接口有大量的标准实现。通常我们使用 LinkedBlockingQueue,它是一个无界队列。LinkedBlockingQueue 包含一个带 int 类型参数的构造方法,它允许我们制定队列的缓存大小。当生产者生产的商品塞满缓存之后,会被阻塞,直到消费者消费了商品之后,才会唤醒它。这样可以保证我们的内存不会被无限的占用。还可以使用 ArrayBlockingQueue,它具有固定的大小。
我们可以这样在生产者——消费者问题中使用阻塞队列。如果消费者任务试图从队列获取对象,而该队列此时为空,那么这些队列还可以挂起消费者任务,并且当条件允许时唤醒消费者任务。以下是阻塞队列使用的简单例子:
1 package ThreadTest.cooperation; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 import java.util.concurrent.ArrayBlockingQueue; 7 import java.util.concurrent.BlockingQueue; 8 import java.util.concurrent.LinkedBlockingQueue; 9 import java.util.concurrent.SynchronousQueue; 10 11 import ThreadTest.LiftOff; 12 13 class LiftOffRunner implements Runnable{ 14 private BlockingQueue<LiftOff> rockets; 15 public LiftOffRunner(BlockingQueue<LiftOff> rockets) { 16 this.rockets=rockets; 17 } 18 public void add(LiftOff lo) { 19 try { 20 rockets.put(lo); 21 } catch (InterruptedException e) { 22 System.out.println("Interrupted during put()"); 23 } 24 } 25 @Override 26 public void run() { 27 try { 28 while(!Thread.interrupted()) { 29 LiftOff rocket = rockets.take(); 30 rocket.run(); 31 } 32 }catch(InterruptedException e) { 33 System.out.println("waking form take()"); 34 } 35 36 } 37 } 38 public class TestBlockingQueues { 39 static void getKey() { 40 try { 41 new BufferedReader(new InputStreamReader(System.in)).readLine(); 42 } catch (IOException e) { 43 throw new RuntimeException(e); 44 } 45 46 } 47 static void getKey(String message) { 48 System.out.println("message"); 49 getKey(); 50 } 51 static void test(String msg,BlockingQueue<LiftOff> queue) { 52 System.out.println("message"); 53 LiftOffRunner runner = new LiftOffRunner(queue); 54 Thread t = new Thread(runner); 55 t.start(); 56 for(int i=0;i<5;i++) { 57 runner.add(new LiftOff(5)); 58 } 59 getKey("press 'Enter' (" + msg + ")"); 60 t.interrupt(); 61 System.out.println("finished " + msg + " test"); 62 } 63 public static void main(String[] args) { 64 test("LinkedBlockingQueue",new LinkedBlockingQueue<LiftOff>()); 65 test("ArrayBlockingQueue",new ArrayBlockingQueue<LiftOff>(3)); 66 test("SynchronousQueue",new SynchronousQueue<LiftOff>()); 67 } 68 69 }
这是一个简单的使用 BlockingQueue 的例子,在 main() 方法中,我们对3中常用的 BlockingQueue 进行了测试,为了输出更明确,我们再实际运行时,需要把另外两个注释掉(每次只测试一个)。当我们测试时,输出会在 5 个 LiftOff 倒计时完成之后停住,并且程序进入阻塞状态,这是因为,take() 方法会取走BlockingQueue 的首位线程并运行,而当首位为空的时候,会进入等待(阻塞)状态,直到有新的数据插入为止。而在该程序中,我们使用了 键入 Enter 的方式来中断 LinkedBlockingQueue。
而且,我们可以注意到,5个 LiftOff 任务是在 t.start() 启动之后放进去的,在没有放进去之前,BlockingQueue 也是出于等待状态。
看到 BlockingQueue 的特性,我们或许会想到,能不能用它来重写生产者——消费者的程序。将商品 Meal 放到 BlockingQueue 中,然后由队列来控制生产者和消费者的唤醒与阻塞工作。以下是利用 BlockingQueue 对 Restaurant 的改写。
1 package ThreadTest.cooperation; 2 3 import java.util.concurrent.ExecutorService; 4 import java.util.concurrent.Executors; 5 import java.util.concurrent.LinkedBlockingQueue; 6 import java.util.concurrent.TimeUnit; 7 8 class Meal{ 9 private final int orderNum; 10 public Meal(int orderNum) { 11 this.orderNum = orderNum; 12 } 13 public String toString() { 14 return "Meal " + orderNum; 15 } 16 } 17 18 class WaitPerson implements Runnable{ 19 20 private Restaurant restaurant; 21 public WaitPerson(Restaurant restaurant) { 22 this.restaurant = restaurant; 23 } 24 @Override 25 public void run() { 26 try { 27 while(!Thread.interrupted()) { 28 System.out.println("Waitperson got " + restaurant.mealQueue.take()); 29 } 30 }catch (InterruptedException e){ 31 System.out.println("Waitperson Interrupted"); 32 } 33 } 34 35 } 36 37 class Chef implements Runnable{ 38 39 private Restaurant restaurant; 40 private int count = 0; 41 public Chef(Restaurant restaurant) { 42 this.restaurant = restaurant; 43 } 44 @Override 45 public void run() { 46 try { 47 while(!Thread.interrupted()) { 48 if(++count== 10) { 49 System.out.println("Out of food, close"); 50 restaurant.exec.shutdownNow(); 51 } 52 System.out.println("Order Up"); 53 Meal meal=new Meal(count); 54 restaurant.mealQueue.put(meal); 55 System.out.println("Meal count: " + count); 56 } 57 }catch(InterruptedException e) { 58 System.out.println("chef interrupt"); 59 } 60 } 61 62 } 63 public class Restaurant { 64 Chef chef = new Chef(this); 65 WaitPerson waitPerson = new WaitPerson(this); 66 LinkedBlockingQueue<Meal> mealQueue = new LinkedBlockingQueue<Meal>(1); 67 ExecutorService exec = Executors.newCachedThreadPool(); 68 public Restaurant() { 69 exec.execute(chef); 70 exec.execute(waitPerson); 71 } 72 public static void main(String[] args) { 73 new Restaurant(); 74 75 } 76 77 } 78 //Output 79 /*Order Up 80 Meal count: 1 81 Order Up 82 Meal count: 2 83 Order Up 84 Waitperson got Meal 1 85 Waitperson got Meal 2 86 Waitperson got Meal 3 87 Meal count: 3 88 Order Up 89 Meal count: 4 90 Order Up 91 Meal count: 5 92 Order Up 93 Waitperson got Meal 4 94 Waitperson got Meal 5 95 Waitperson got Meal 6 96 Meal count: 6 97 Order Up 98 Meal count: 7 99 Order Up 100 Meal count: 8 101 Order Up 102 Waitperson got Meal 7 103 Waitperson got Meal 8 104 Waitperson got Meal 9 105 Meal count: 9 106 Out of food, close 107 Waitperson Interrupted 108 Order Up 109 chef interrupt 110 */
在上述程序中,我们完全抛弃了 wait() 和 notify() 将工作完全交给了 BlockingQueue 内部去完成。这就是 BlockingQueue 的强大之处。
七、任务间使用管道进行输入/输出
通过输入/输出在线程间进行通信通常很有用。提供线程功能的类库以“管道”的形式对线程间的输入/输出提供支持。它们在 java 输入/输出类库中对应物就是 PipedWriter 类(允许任务向管道写)和 PipedReader 类(允许不同任务从同一管道中读取)。这个模型可以看成是“生产者——消费者”问题的变体,这里管道就是一个封装好的解决方案。管道基本上是一个阻塞队列。
下面是一个简单的列子,两个任务使用一个管道进行通信。
1 package ThreadTest.cooperation; 2 3 import java.io.IOException; 4 import java.io.PipedReader; 5 import java.io.PipedWriter; 6 import java.util.Random; 7 import java.util.concurrent.ExecutorService; 8 import java.util.concurrent.Executors; 9 import java.util.concurrent.TimeUnit; 10 11 class Sender implements Runnable{ 12 private Random rand = new Random(47); 13 private PipedWriter out = new PipedWriter(); 14 public PipedWriter getPipedWriter() {return out;} 15 public void run() { 16 try { 17 while(true) { 18 for(char c = 'A';c<='z';c++) { 19 out.write(c); 20 TimeUnit.MICROSECONDS.sleep(rand.nextInt(500)); 21 } 22 } 23 }catch(IOException e) { 24 System.out.println(e+" Sender write Exception"); 25 }catch(InterruptedException e) { 26 System.out.println(e+" Sender sleep interrupted"); 27 } 28 } 29 } 30 31 class Receiver implements Runnable{ 32 private PipedReader in; 33 public Receiver(Sender sender) throws IOException { 34 this.in = new PipedReader(sender.getPipedWriter()); 35 } 36 public void run() { 37 try { 38 while(true) { 39 System.out.println("Read: " + (char)in.read()+","); 40 } 41 }catch(IOException e) { 42 System.out.println(e+" Receiver read Exception"); 43 } 44 } 45 } 46 public class PipedIO { 47 public static void main(String[] args) throws IOException, InterruptedException { 48 Sender sender = new Sender(); 49 Receiver receiver = new Receiver(sender); 50 ExecutorService exec = Executors.newCachedThreadPool(); 51 exec.execute(sender); 52 exec.execute(receiver); 53 TimeUnit.SECONDS.sleep(5); 54 exec.shutdownNow(); 55 } 56 }
不难发现,Piped~ 类和BlockingQueue 非常相似,都具备自动阻塞和唤醒任务的功能。其实在BlockingQueue 出现之前,Piped~ 类就在前几个版本出现了。
八、死锁
在锁的那一章,我们了解到,当多个任务共享同一个资源时,当某个任务先获取到该资源,其他任务必须等到该任务使用完之后释放锁才能继续进行自己的任务。那么会不会出现这样一种情况,A需要等待B完成,B需要等待C完成...N又需要等待A完成。多个任务形成了一个死循环,这种情况我们称为死锁。
关于死锁,最经典的莫过于哲学家吃饭问题,我们接下来就来讨论下这个问题。
5个哲学家,围绕着一个圆桌而坐,每两个人中间都会有一只筷子,也就是说每个哲学家的左右边都会有一只筷子(也就是说一共5只筷子)。当某位哲学家想吃饭时,他会去尝试拿起左手和右手的筷子,如果两只筷子有一只或者两只已经被其他哲学家拿起,那么他就会等待,直到其他人吃完,放下筷子,他开始吃饭。
通过问题的描述,我们可以建立这样一个模型,5个哲学家、5根筷子。哲学家有两个方法,吃饭和休息。筷子也有两个方法,拿起和放下。我们给5个哲学家和筷子分别编号 !~5 那么 1 号哲学家左手是1号筷子,右手是2号筷子。2号哲学家左手是2号筷子,右手是3号筷子,以此类推...5号哲学家左手是5号筷子,右手是1号筷子。
哲学家的吃饭和睡觉状态是随机的,当1号哲学家想要吃饭时,会调用1号筷子和2号筷子的拿起方法,吃完饭后会放下两只筷子。筷子的拿起的方法是上锁的,确保每根筷子每次只能被一个哲学家拿起。如果这时候2号哲学家也想吃饭,它会在尝试拿起2号筷子时被阻塞,但是他可以拿起3号筷子,等到1号哲学家吃完,2号筷子被放下,2号哲学家就可以开始用餐了。以下是该问题的代码实现:
1 package ThreadTest.cooperation; 2 3 import java.io.IOException; 4 import java.util.Random; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.TimeUnit; 8 9 class Chopstick{ 10 private final int id; 11 public Chopstick(int id) { 12 this.id = id; 13 } 14 private boolean taken = false;//是否被拿起 15 public synchronized void take() throws InterruptedException { 16 while(taken) wait(); 17 taken = true; 18 } 19 20 public synchronized void drop(){ 21 taken = false; 22 notifyAll(); 23 } 24 } 25 26 class Philosopher implements Runnable{ 27 private Chopstick left; 28 private Chopstick right; 29 private final int id; 30 private final int ponderFactor; 31 private Random rand = new Random(47); 32 private void pause() throws InterruptedException { 33 if(ponderFactor == 0) return; 34 TimeUnit.MILLISECONDS.sleep(rand.nextInt(ponderFactor * 500)); 35 } 36 public Philosopher(Chopstick left,Chopstick right,int ident,int ponder) { 37 this.left = left; 38 this.right = right; 39 this.id = ident; 40 this.ponderFactor = ponder; 41 } 42 @Override 43 public void run() { 44 try { 45 while(!Thread.interrupted()) { 46 System.out.println(this + " thinking" ); 47 pause(); 48 left.take(); 49 System.out.println(this + " grabbing right"); 50 right.take(); 51 System.out.println(this + " grabbing left"); 52 System.out.println(this + " eating"); 53 pause(); 54 right.drop(); 55 System.out.println(this + " drop right"); 56 left.drop(); 57 System.out.println(this + " drop left"); 58 } 59 }catch(InterruptedException e) { 60 System.out.println(this + " exiting via interrupted"); 61 } 62 } 63 64 public String toString() { 65 return "Philosopher : " + id; 66 } 67 68 } 69 public class DeadLockingDiningPhilosophers { 70 71 public static void main(String[] args) throws IOException { 72 int ponder = 5; 73 int size = 5; 74 Chopstick[] sticks = new Chopstick[size]; 75 Philosopher[] philosophers = new Philosopher[size]; 76 ExecutorService exec = Executors.newCachedThreadPool(); 77 for(int i=0;i<size;i++) { 78 sticks[i] = new Chopstick(i); 79 } 80 for(int i=0;i<size;i++) { 81 philosophers[i] = new Philosopher(sticks[i],sticks[(i+1)%size],i,ponder); 82 exec.execute(philosophers[i]); 83 } 84 System.out.println("press Enter to quit"); 85 System.in.read(); 86 exec.shutdownNow(); 87 } 88 89 } 90 //output 91 /*Philosopher : 0 thinking 92 press Enter to quit 93 Philosopher : 3 thinking 94 Philosopher : 2 thinking 95 Philosopher : 1 thinking 96 Philosopher : 4 thinking 97 Philosopher : 0 grabbing right 98 Philosopher : 4 grabbing right 99 Philosopher : 3 grabbing right 100 Philosopher : 2 grabbing right 101 Philosopher : 1 grabbing right*/
当哲学家们都拿起自己右手的筷子时,程序就进入了死锁。
关于死锁的发生,有下面4个条件:
- 互斥条件。任务使用的资源中至少一个资源是不能共享的。
- 资源不能被任务抢占,即哲学家不能从其他人手里抢夺筷子。
- 至少有一个任务它必须持有一个资源,且正在等待获取一个当前被另一个任务持有的资源。
- 必须有循环等待。
那么解决死锁的方案就是使上面4个条件至少一个不成立,而最容易破坏的是第4个条件。在哲学家吃饭的例子里。如果最后一个哲学家不是先拿右手边的筷子,而是先拿左手,就不会产生等待的循环。
以下是优化后的代码:
1 package ThreadTest.cooperation; 2 3 import java.io.IOException; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 7 public class FixedDiningPhilosophers { 8 public static void main(String[] args) throws IOException { 9 int ponder = 5; 10 int size = 5; 11 Chopstick[] sticks = new Chopstick[size]; 12 Philosopher[] philosophers = new Philosopher[size]; 13 ExecutorService exec = Executors.newCachedThreadPool(); 14 for(int i=0;i<size;i++) { 15 sticks[i] = new Chopstick(i); 16 } 17 for(int i=0;i<size-1;i++) { 18 philosophers[i] = new Philosopher(sticks[i],sticks[(i+1)%size],i,ponder); 19 exec.execute(philosophers[i]); 20 } 21 philosophers[size-1] = new Philosopher(sticks[size%size],sticks[(size-1)],size-1,ponder); 22 exec.execute(philosophers[size-1]); 23 System.out.println("press Enter to quit"); 24 System.in.read(); 25 exec.shutdownNow(); 26 } 27 }