JAVA并发编程6_线程协作/生产者-消费者
前面通过同步锁来同步任务的行为,两个任务在交替访问共享资源的时候,可以通过使用同步锁使得任何时候只有一个任务可以访问该资源,见博客:线程同步之synchronized关键字。下面主要讲的是如何使任务彼此间可以协作,使得多个任务可以一起工作去解决木某个问题,因为有些问题中,某些部分必须在其他部分被解决之前解决,就像在餐厅服务员要端菜就必须有厨师做好了菜。在任务协作时,可以让任务自身挂起,直至某些外部条件发生变化,表示是时候让这个任务向前推动了为止。
wait/notify
wait方法会在等待外部世界产生变化的时候将任务挂起,并且只有在notify或notifyAll发生时即表示发生了某些感兴趣的事物,这个任务才会被唤醒并去检查所产生的变化。
wait方法表示主动释放同步锁并将任务挂起,当前任务处于waiting状态。由于会释放锁,意味着该对象的其他synchronized方法可以在wait期间被调用。而恰好在这些方法里面发生了唤醒被挂起任务所感兴趣的变化。
当调用wait方法时,就是再声明:“我已经刚刚做完能做的所有事情,因此我要在这里等待,但是我希望其他的synchronized操作在条件适合的情况下通知我让我继续执行。”
notify方法用来唤醒处于wait状态的任务,当notify/notifyAll因某个特定的锁而被调用时,只有等待这个锁的任务才会被唤醒。
注:
wait/notify/notifyAll这些方法是基类Object的一部分。因为任何Object都可以作为同步锁。并且只能在同步控制方法或者同步代码块里面才能调用这些方法,如果在非同步方法里面调用,程序能通过编译,运行的时候会报错IllegalMonitorStateException。也就是说调用这些方法必须拥有对象锁(同步锁)。
生产者消费者
在一个饭店里,有一个厨师和一个服务员,这个服务员必须等待厨师准备好膳食,当厨师准备好时,他会通知服务员,之后服务员上菜,然后返回继续等待。这是一个任务协作的实例。厨师代表生产者,而服务员代表消费者。
class Restaurant{ private Object obj; public void put(){ while (true) { synchronized (this) { while (obj != null) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } obj = new Object(); System.out.print("Order up! "); notifyAll(); } } } public void get(){ while (true) { synchronized (this) { while (obj == null) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("waiter get meal"); obj = null; notifyAll(); } } } } class Chef extends Thread{ private Restaurant restaurant; public Chef(Restaurant restaurant){ this.restaurant = restaurant; } @Override public void run() { restaurant.put(); } } class Waiter extends Thread{ private Restaurant restaurant; public Waiter(Restaurant restaurant){ this.restaurant = restaurant; } @Override public void run() { restaurant.get(); } } public class Test { public static void main(String[] args) { Restaurant restaurant = new Restaurant(); Chef produce = new Chef(restaurant); Waiter consumer = new Waiter(restaurant); produce.start(); consumer.start(); } }输出:
Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal Order up! waiter get meal ...
Chef和Waiter通过Resaurant打交道,在两个线程中,一个线程也就是厨师线程挂起等待服务员线程消费做好的meal,他所感兴趣的事物就是obj是否为null。一旦obj为null也就是服务员将这份meal消费了,就会唤醒服务员消费这份meal。
另一个线程也就是服务员线程挂起等待厨师做好meal,他所感兴趣的事物也是obj是否为null。一旦obj不为null,他会消费这份meal,并且唤醒厨师做下一份meal。
一般,wait会被包装在while()语句中,这个语句在不断地测试正在等待的事物。可否换成if?初步看上去,一旦被唤醒,这个obj必然时刻获得的,因为任务被唤醒就是发生了其感兴趣的事物,可是,正事由于问题发生在并发应用中。其他任务有可能会在当前任务被唤醒时,突然插足拿走obj。因为常用下面wait的惯用法
while(conditiopnIsNotMet)
wait();
这样才能更加安全,保证了在退出循环之前条件得到满足。
added at 2016年1月1日
最近看操作系统,有了新的体会,于是加入下面的内容。首先将上面的例子添加一个缓冲区,大小为10,并用信号量同步
/** * 使用信号量来实现生产者消费者 * 操作系统中信号量是由操作系统内核实现的PV原语,保证p、v操作的原子性 * 使用信号量如果占用了某个信号量而处于阻塞状态,那么本进程/线程奖杯加入阻塞队列,只有等待其他的进程/线程唤醒。 * 和管程不一样,管程里面,如果不小心获得了临界区的使用权,但是由于其他原因未获得相关资源时,可以主动放弃使用权。这导致了管程编写同步代码更方便 * java支持管程,通过条件变量的wait/notify * 看操作系统的理解,不一定正确 */ class Restaurant{ List<Object> list = new ArrayList<Object>(); Semaphore fullSemaphore = new Semaphore(0); Semaphore emptySemaphore = new Semaphore(10); Semaphore mutex = new Semaphore(1); public void put() throws InterruptedException{ while (true) { Object obj = new Object(); emptySemaphore.acquire(); mutex.acquire(); list.add(obj); System.out.println("生产一个数据,这时缓冲区有" + list.size() + "个数据"); Thread.sleep(100); mutex.release(); fullSemaphore.release(); } } public void get() throws InterruptedException{ while (true) { fullSemaphore.acquire(); mutex.acquire();// 如果放在上面,刚开始进来,获得使用权,但是fullSemaphore-1=-1<0,将加入阻塞队列。这时生产者会会因为这里释放不了临界区的使用权而阻塞在获取使用权那里。 Object obj = list.remove(list.size()-1); System.out.println("消费一个数据,这时缓冲区有" + list.size() + "个数据"); Thread.sleep(100); mutex.release(); emptySemaphore.release(); // 消费这个obj } } } class Chef extends Thread{ private Restaurant restaurant; public Chef(Restaurant restaurant){ this.restaurant = restaurant; } @Override public void run() { try { restaurant.put(); } catch (InterruptedException e) { e.printStackTrace(); } } } class Waiter extends Thread{ private Restaurant restaurant; public Waiter(Restaurant restaurant){ this.restaurant = restaurant; } @Override public void run() { try { restaurant.get(); } catch (InterruptedException e) { e.printStackTrace(); } } } public class Test { public static void main(String[] args) { Restaurant restaurant = new Restaurant(); Chef produce = new Chef(restaurant); Waiter consumer = new Waiter(restaurant); produce.start(); consumer.start(); } }输出
生产一个数据,这时缓冲区有1个数据 生产一个数据,这时缓冲区有2个数据 生产一个数据,这时缓冲区有3个数据 生产一个数据,这时缓冲区有4个数据 消费一个数据,这时缓冲区有3个数据 生产一个数据,这时缓冲区有4个数据 生产一个数据,这时缓冲区有5个数据 消费一个数据,这时缓冲区有4个数据 消费一个数据,这时缓冲区有3个数据 消费一个数据,这时缓冲区有2个数据 消费一个数据,这时缓冲区有1个数据 消费一个数据,这时缓冲区有0个数据 生产一个数据,这时缓冲区有1个数据 消费一个数据,这时缓冲区有0个数据 生产一个数据,这时缓冲区有1个数据 消费一个数据,这时缓冲区有0个数据 生产一个数据,这时缓冲区有1个数据 ...操作系统中信号量是由操作系统内核实现的PV原语,保证p、v操作的原子性。使用信号量如果占用了某个信号量而处于阻塞状态,那么本进程/线程奖杯加入阻塞队列,只有等待其他的进程/线程唤醒。和管程不一样,管程里面,如果不小心获得了临界区的使用权,但是由于其他原因未获得相关资源时,可以主动放弃使用权。这导致了管程编写同步代码更方便java支持管程,通过条件变量的wait/notify
java中类似于管程的机制实现同步
/** * java中支持的类似于管程的机制 */ class Restaurant{ List<Object> list = new ArrayList<Object>(); public void put() throws InterruptedException{ while (true) { synchronized (this) { while (list.size() == 10) {// wait(); } Object obj = new Object(); list.add(obj); System.out.println("生产一个数据,这时缓冲区有" + list.size() + "个数据"); Thread.sleep(100); if (list.size() == 1) { notify(); } } } } public void get() throws InterruptedException{ while (true) { synchronized (this) { while (list.size() == 0) { wait(); } list.remove(list.size()-1); System.out.println("消费一个数据,这时缓冲区有" + list.size() + "个数据"); Thread.sleep(100); if (list.size() == 1) { notify(); } } } } } class Chef extends Thread{ private Restaurant restaurant; public Chef(Restaurant restaurant){ this.restaurant = restaurant; } @Override public void run() { try { restaurant.put(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } class Waiter extends Thread{ private Restaurant restaurant; public Waiter(Restaurant restaurant){ this.restaurant = restaurant; } @Override public void run() { try { restaurant.get(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public class Test { public static void main(String[] args) { Restaurant restaurant = new Restaurant(); Chef produce = new Chef(restaurant); Waiter consumer = new Waiter(restaurant); produce.start(); consumer.start(); } }
输出
生产一个数据,这时缓冲区有1个数据 消费一个数据,这时缓冲区有0个数据 生产一个数据,这时缓冲区有1个数据 生产一个数据,这时缓冲区有2个数据 生产一个数据,这时缓冲区有3个数据 生产一个数据,这时缓冲区有4个数据 生产一个数据,这时缓冲区有5个数据 生产一个数据,这时缓冲区有6个数据 生产一个数据,这时缓冲区有7个数据 消费一个数据,这时缓冲区有6个数据 消费一个数据,这时缓冲区有5个数据 消费一个数据,这时缓冲区有4个数据 消费一个数据,这时缓冲区有3个数据 生产一个数据,这时缓冲区有4个数据 消费一个数据,这时缓冲区有3个数据 消费一个数据,这时缓冲区有2个数据 消费一个数据,这时缓冲区有1个数据 消费一个数据,这时缓冲区有0个数据 生产一个数据,这时缓冲区有1个数据
hansen管程:while,条件变量的释放是一个提示,需要重新检查条件。当前执行的线程优先。
hoare管程:if,条件变量释放同时表示放弃管程访问,释放后条件变量的状态可用