引言:生产者消费者问题是一个十分经典的多线程问题。为了更加形象地描述这个问题,采用可视化的形式展示此过程。
1、问题重述
生产者消费者问题也称有限缓冲问题。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
当我们在思考如何解决这个问题的时候一定要联系实际。实际生活中(不考虑直接点对点模式),工厂生产的商品一般会通过经销商销售,消费者从经销商中购买商品。经销商手中的存货是有限的,同时他需要的商品也是有限的。当他不需要商品的时候,生产商不能再生产更多商品;当他没有存货的时候,消费者无法从他手中购买。这里,经销商就相当于一个缓冲区。
这种生产者-消费者模式有什么好处呢?使用这种模式,可以让消费者、生产者互相独立,生产者不需要依赖消费者的消费速度,它只关心缓冲区的状况。
2、前期准备
实现方法有四种:- wait()和notify()方法
- await()和signal()方法
- 阻塞队列方法
- 管道方法
- 此教程采用第一种方法。
2.1 synchronized
多线程并发存在着线程安全问题,主要在于存在共享数据以及多个线程共同操作共享数据。使用synchronized可以保证线程互斥的访问代码。其原理在于它可以保证方法或者代码块在同一时刻只有一个可以进入到临界区,还可以保证共享变量的内存可见性
synchronized一般称"同步锁",在修饰代码块的时候需要传入一个对象作为"锁"的对象。线程同步就是利用锁机制先给共享资源上锁,只有拿到锁的线程才可以访问共享资源,其他线程进入等待状态
2.2 wait()方法
wait方法()是Object类的方法,其作用是使当前执行代码的线程进入等待。在调用wait()方法之前,线程必须获得该对象的锁,即只能在同步方法或同步块中调用wait()方法。wait()方法执行之后,当前线程释放锁。从wait()返回之前,线程会与其他线程产生资源竞争。当调用wait()时,如果没有锁将会抛出异常
2.3 notify()方法与notifyAll()方法
notify()方法用来通知那些可能等待该对象的对象锁的其他线程,若有多个线程,则随机选择wait状态的线程对其notify,使它获得该对象的对象锁。但是,线程不会马上释放锁,而是等到执行notify()方法的线程将程序执行完后即退出synchronized语句块后,该线程才会释放锁。注意,操作不当可能会出现过早通知
notifyAll()使所有线程退出wait状态
总之,简单来讲:wait()使线程停止运行,notify()使线程继续运行
3、实现消费者-生产者模型
3.1 界面设计
创建Frame类和UI类,Frame创建窗体,UI添加控件以及绑定事件。该部分只介绍布局,绑定事件后面再细讲。用进度条代表生产/消费进度。界面如下图所示:class Frame extends JFrame { public int width=1500; public int height=1500; UI ui=new UI(); public Container container; public Frame() { setTitle("生产者消费者问题"); setSize(width,height); setLocation(300,0); container=getContentPane(); container.add(ui); } }
class UI extends JPanel { public static JProgressBar producerBar;//表示生产者生产进度 public static JLabel producerJLabel;//表示生产者生产个数 public static JLabel bufferJLabel;//缓冲区 public static JLabel amountJLabel;//缓冲区个数 public static JProgressBar consumerBar;//表示消费者消费速度 public static JLabel consumerJLabel;//表示消费者消费个数 JButton bt1;//生产者开始生产 JButton bt2;//生产者停止生产 JButton bt3;//消费者开始消费 JButton bt4;//消费者停止消费 public UI() { setLayout(null); setSize(1400,1400); producerBar=new JProgressBar(); consumerBar=new JProgressBar(); bt1=new JButton("开始生产"); bt2=new JButton("停止生产"); bt3=new JButton("开始消费"); bt4=new JButton("停止消费"); producerJLabel=new JLabel("生产者"); consumerJLabel=new JLabel("消费者"); bufferJLabel=new JLabel("缓冲区 (最大容量25)"); amountJLabel=new JLabel("商品数量:0"); producerBar.setBackground(Color.WHITE); producerBar.setForeground(Color.BLACK); consumerBar.setBackground(Color.WHITE); producerBar.setForeground(Color.BLACK); bt1.setBounds(50,170,70,60); bt2.setBounds(150,170,70,60); bt3.setBounds(950, 700, 70, 60); bt4.setBounds(1050,700,70,60); bufferJLabel.setBounds(550,350,200,200); amountJLabel.setBounds(550,300,200,400); producerJLabel.setBounds(50,70,50,50); producerBar.setBounds(50, 120, 480, 40); consumerJLabel.setBounds(950,600,50,50); consumerBar.setBounds(950,650,480,40); add(producerBar); add(producerJLabel); add(consumerBar); add(consumerJLabel); add(bt1); add(bt2); add(bt3); add(bt4); add(bufferJLabel); add(amountJLabel); } }
3.2 模型设计
根据问题描述即可知道设计思路,创建三个类分别是:Buffer类(缓冲区)、Producer类(生产者)、Consumer类(消费者)。在Buffer类里面我们要模仿生产者生产商品以及消费者消费商品,这里就产生了共享资源——商品。因此,我们需要用到synchronized锁保证线程安全,采用wait()/notify()方法。注意到一点,在消费者-生产者模式下,生产者在缓冲区未满的情况下只管不断生产,消费者在缓冲区未空的情况下只管不断消费。3.2.2 Buffer类
在Buffer类,我们需要实现的是模拟商品运到仓库和商品从仓库出去以及相应的UI。UI方面,需要实时显示缓冲区商品数量。class Buffer { private static final int max=25;//缓冲区最大容量 private LinkedList<Object>list=new LinkedList<Object>();//表示商品实体 JLabel amount;//缓冲区当前商品数量 public Buffer(JLabel amount) { this.amount=amount; } public synchronized void produce()// { while(list.size()==max)//当缓冲区达到最大容量时,如果不释放资源,则生产进程一直处于阻塞状态 { amount.setText("缓冲区已满,生产阻塞"); try { wait();//生产阻塞 } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } //没有达到缓冲区最大容量 list.add(new Object());//生产商品 amount.setText("商品数量: "+list.size()); notifyAll();//当生产一个商品之后,可以唤醒其他线程 } public synchronized void consume() { while(list.size()==0)//当缓冲区为空时 { amount.setText("缓冲区已空,消费阻塞"); try { wait(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } list.remove();//消费商品 amount.setText("商品数量: "+list.size()); notifyAll(); } }
3.2.3 Producer类
用进度条模拟消费者不断生产的过程class Producer implements Runnable { Buffer buffer; JProgressBar produceBar;//生产进度条 int i; public Producer(Buffer buffer,JProgressBar produceBar) { // TODO Auto-generated constructor stub this.buffer=buffer; this.produceBar=produceBar; } @Override public void run() { while(true) { try { if(i<=25) { i++; produceBar.setValue(i*4); try { Thread.sleep(200); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } buffer.produce(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } } }
3.2.4 Consumer类
用进度条模拟消费者不断消费的过程class Consumer implements Runnable { Buffer buffer; JProgressBar consumeBar;//消费进度条 int i; public Consumer(Buffer buffer,JProgressBar consumeBar) { // TODO Auto-generated constructor stub this.buffer=buffer; this.consumeBar=consumeBar; } @Override public void run() { while(true) { try { if(i<=25) { i++; consumeBar.setValue(i*4); try { Thread.sleep(200); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } buffer.consume(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } } }
3.2.5 事件绑定
主要是通过按钮来控制生产/消费的开始与暂停。在UI类的构造函数里面实例化对象,并为按钮绑定监听器。同时申请两个线程,分别控制生产行为和消费行为。Buffer buffer;//缓冲区 Producer producer;//生产者 Consumer consumer;//消费者 Thread thread1;//线程1用于控制生产行为的开始与暂停 Thread thread2;//线程2用于控制消费行为的开始与暂停 buffer=new Buffer(amountJLabel); public UI() { bt1.addActionListener(new ActionListener() {//开始生产 @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub if(thread1!=null) { try { thread1.stop(); } catch (Exception e2) { // TODO: handle exception } } thread1=new Thread(new Producer(buffer,producerBar)); thread1.start(); } }); bt2.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub if(thread1!=null) { try { thread1.stop(); } catch (Exception e2) { // TODO: handle exception e2.printStackTrace(); } } } }); bt3.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub if(thread2!=null) { try { thread2.stop(); } catch (Exception e2) { // TODO: handle exception e2.printStackTrace(); } } thread2=new Thread(new Consumer(buffer,consumerBar)); thread2.start(); } }); bt4.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub if(thread2!=null) { try { thread2.stop(); } catch (Exception e2) { // TODO: handle exception e2.printStackTrace(); } } } }); }