因为缓存区是共享的数据结构,就必须使用一种同步机制来控制访问,例如synchronized关键字,但是在这个问题中有更多的限制。如果缓存区已满,生产者将无法保存数据,同时如果缓存区已空,消费者也无法取走数据。
为了应对这些情况,Java提供了在Object类中实现的wait()、notify()、和notifyAll()方法。线程能够在synchronized代码块中调用wait()方法,如果它在synchronized代码块之外调用wait()方法的话,Java虚拟机抛出IllegalMonitorStateException异常。当线程调用wait()方法时,Java虚拟机让线程休眠,释放控制正在执行的synchronized代码块的对象,允许其它线程来执行剩下的被这个对象保护的sychronized代码块。想要唤醒线程,必须在被相同对象保护的代码块中调用notify()或者notifyAll()方法。
在本节中,学习如何使用synchronized关键字和wait()、notify()\notifyAll()方法来实现生产者-消费者问题。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤完成范例:
-
创建名为EventStorage的类,定义两个变量,分别是整型的maxSize和List类型的storage:
public class EventStorage { private int maxSize; private Queue<Date> storage;
-
实现类的构造函数,初始化类变量:
public EventStorage() { maxSize = 10; storage = new LinkedList<>(); }
-
实现synchronized方法set(),存储一个事件到变量storage中。首先,判断变量是否为空,如果不为空,就调用wait()方法,直到storatge有空间。在方法结尾,调用notify()方法唤醒所有在wait()方法中休眠的线程。在本范例中,忽略InterruptedException异常。但在真正的实现过程中,必须要考虑如何处理异常,可以将应用中的异常重新抛出或者转移成其它类型的异常:
public synchronized void set(){ while (storage.size() == maxSize){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } storage.offer(new Date()); System.out.printf("Set: %d\n", storage.size()); notify(); }
-
实现synchronized方法get(),因为存储目的获得一个事件。首先,判断变量是否包含事件。如果没有事件,调用wait()直到变量中保存了一些事件。在方法结尾,调用notify()方法唤醒所有在wait()方法中休眠的线程。在本范例中,忽略InterruptedException异常。但在真正的实现过程中,必须要考虑如何处理异常,可以将应用中的异常重新抛出或者转移成其它类型的异常:
public synchronized void get(){ while(storage.size() == 0){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } String element = storage.poll().toString(); System.out.printf("Get: %d: %s\n",storage.size(), element); notify(); }
-
创建名为Producer的类并且指定其实现Runnable接口,此类将实现范例中的生产者:
public class Producer implements Runnable{
-
定义一个EventStore对象,实现类的构造函数,并初始化对象:
private EventStorage storage; public Producer(EventStorage storage) { this.storage = storage; }
-
实现run()方法,调用EventStorage对象中的set()方法100次:
@Override public void run() { for (int i = 0; i < 100 ; i++){ storage.set(); } }
-
创建名为Consumer的类并且指定其实现Runnable接口,此类将实现范例中的消费者:
public class Consumer implements Runnable{
-
定义一个EventStore对象,实现类的构造函数,并初始化对象:
private EventStorage storage; public Consumer(EventStorage storage){ this.storage = storage; }
-
实现run()方法,调用EventStorage对象中的get()方法100次:
@Override public void run() { for(int i = 0 ; i < 100 ; i ++){ storage.get(); } }
-
创建本范例中的主类,实现一个包含main()方法的Main类:
public class Main { public static void main(String[] args) {
-
创建EventStore对象:
EventStorage storage = new EventStorage();
-
创建Producer对象以及运行此对象的线程:
Producer producer = new Producer(storage); Thread thread1 = new Thread(producer);
-
创建Consumer对象以及运行此对象的线程:
Consumer consumer = new Consumer(storage); Thread thread2 = new Thread(consumer);
-
启动两个线程:
thread2.start(); thread1.start();
###工作原理
本范例中的关键点是EventStorage类中的set()和get()方法。首先,set()方法检查存储属性中是否有空闲空间。如果空间已满,线程将调用wait()方法等待空闲空间。当其它线程调用notify()方法时,此线程重新启动并且再次检查是否有空闲空间。notify()方法并不保证符合状态。这个检查过程重复进行直到存储变量中有空闲空间,并且可以生成一个新的事件来存储。
get()方法性质相似。首先,检查存储属性中是否有事件。如果EventStorage类为空,线程调用wait()方法等待事件。当其它线程调用notify()方法时,此线程重新启动并且再次检查存储属性中是否有事件,直到非空为止。
在while循环中持续检查状态并且调用wait()方法,当状态为true时,循环才终止。
如果运行此范例,你会发现尽管生产者和消费者不停的保存和取出事件,存储属性始终具有包含超过10个事件的能力。
扩展学习
synchronized关键字还有其它重要用法,查看”更多关注“中的小节内容。
更多关注
- 本章中”同步方法“小节。