Thinking in Java---线程通信+三种方式实现生产者消费者问题
来源:互联网 发布:排施工计划软件 编辑:程序博客网 时间:2024/06/18 18:30
前面讲过线程之间的同步问题;同步问题主要是为了保证对共享资源的并发访问不会出错,主要的思想是一次只让一个线程去访问共享资源,我们是通过加锁的方法实现。但是有时候我们还需要安排几个线程的执行次序,而在系统内部线程的调度是透明的,没有办法准确的控制线程的切换。所以Java提供了一种机制来保证线程之间的协调运行,这也就是我们所说的线程调度。在下面我们会介绍三种用于线程通信的方式,并且每种方式都会使用生产者消费者问题进行检验。
一。使用Object类提供的线程通信机制
Object类提供了wait(),notify(),notifyAll()三个方法进行线程通信。这三个方法都必须要由同步监视器对象来调用,具体由分为以下两种情况:
1)对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
2)对于使用synchronized修饰的同步代码块,同步监视器是synchronized后面括号里的对象,所以必须使用该对象调用这三个方法。
也就是说,这三个方法只能用于synchronized做同步的线程通信。对着三个方法的具体解释如下:
wait():导致当前线程等待,直到其它线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程。该wait()方法还可以传入一个时间参数,这时候等到指定时间后就会自动苏醒。调用wait()方法的当前线程会释放对该同步监视器的锁定。
notify():唤醒在此同步监视器上等待的单个线程。如果当前有多个线程在等待,则随机选择一个。注意只有当前线程放弃对该同步监视器的锁定以后(使用了wait()方法),才可以执行被唤醒的线程。
notifyAll():唤醒在此同步监视器上等待的所有线程。同样只要在当前线程放弃对同步监视器的锁定之后,才可以执行被唤醒的线程。
使用这种通信机制模拟的生产者消费者问题如下:
package lkl1;///生产者消费者中对应的缓冲区//生产者可以向缓冲区中加入数据,消费者可以消耗掉缓冲区中的数据//注意到缓冲区是限定了大小的,所以使用循环队列的思想进行模拟public class Buffer {//根据循环队列的思想,如果out==in,则表示当前缓冲区为空,不可以进行消费 //如果(in+1)%n==out,则表示当前缓冲区为满,不可以进行生产(这样会浪费一个空间) private int n; ///缓冲区大小 private int num; //当前元素个数 //定义一个大小为n的缓冲区 private int buffer[]; //表示当前可以放置数据的位置,初始为0 private int in=0; //表示当前可以读取数据的位置,初始为0 private int out=0; Buffer(int n){ this.n=n; buffer=new int[n]; num=0; } //下面是生产和消费的方法 //生产操作,向缓冲区中加入一个元素x public synchronized void product(int x){ try{ if((in+1)%n==out){ wait(); //如果缓冲区已满,则阻塞当前线程 } else{ buffer[in]=x; in=(in+1)%n; System.out.println(Thread.currentThread().getName()+"生产一个元素: "+x); num++; System.out.println("当前元素个数为: "+num); notifyAll(); //唤醒等待当前同步资源监视器的线程 } } catch(InterruptedException ex){ ex.printStackTrace(); } } ///消费操作,一次取出一个元素 public synchronized void comsumer(){ try{ if(in==out){ //如果缓冲区为空,阻塞当前线程 wait(); } else{ int xx=buffer[out]; out=(out+1)%n; num--; System.out.println(Thread.currentThread().getName()+"消费了一个元素: "+xx); System.out.println("当前元素个数为: "+num); notifyAll(); } } catch(InterruptedException ex){ ex.printStackTrace(); } }}
package lkl1;import java.util.Random;//生产者线程//会不断的往缓冲区中加入元素public class Product extends Thread{ //当前线程操作的缓冲区对象 private Buffer buffer; private Random rand; Product(){} Product(Buffer buffer){ this.buffer=buffer; rand=new Random(); } public void run(){ while(true){ //向缓冲区中添加一个随机数 buffer.product(rand.nextInt(100)); } }}
package lkl1;//生产者线程public class Consumer extends Thread{ private Buffer buffer; Consumer(){} Consumer(Buffer buffer){ this.buffer=buffer; } public void run(){ while(true){ //每次都消耗掉缓冲区中的一个元素 buffer.comsumer(); } }}
package lkl1;//测试public class BufferTest { public static void main(String[] args){ Buffer buffer = new Buffer(10); //一个生产者,多个消费者 new Product(buffer).start(); new Consumer(buffer).start(); new Consumer(buffer).start(); }}
二。使用Condition控制线程通信
前面我们讲同步方式的时候,除了synchronized关键字,还讲了可以使用Lock进行显示的加锁。在使用Lock对象时,是不存在隐式的同步监视器的,所以也就不能使用上面的线程通信方式了。其实在使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition类可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象。
Condition提供了三个方法:await(),signal(),signallAll();这三个方法和Object对象的三个方法的基本用法是一样的。其实我们可以这样认为,Lock对象对应了我们上面讲的同步方法或同步代码块,而Condition对象对应了我们上面讲的同步监视器。还要注意的是,Condition实例被绑定在一个Lock对象上,要获得指定Lock的Condition实例,需要调用Lock对象的newCondtion()方法即可。
下面使用Lock和Condition的组合来实现生产者消费者问题。可以看到代码基本和上面是一样的。
package lkl1;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;///生产者消费者中的缓冲区//由一个数组代表,生产者可以向缓冲区中加入元素,消费者可以从缓冲区中取走元素//如果缓冲区满,则生产者不能向缓冲区加入元素;如果缓冲区空,则消费者不能消费元素//下面的程序中in表示生产者可以加入数据的位置,out表示消费者可以消费数据的位置//in和out都会初始化为0,我们定义in==out表示缓冲区为空;(in+1)%n==out//表示缓冲区满,但是这种判满的方式是要浪费一个空间的。//上个例子中使用了synchronized关键字保证对缓冲区的操作的同步。//现在需要采用Lock和Condition类进行同步的控制.public class Buffer1 { private final Lock lock=new ReentrantLock(); private final Condition con=lock.newCondition(); private int n; private int buffer1[]; private int in; private int out; private int cnt; ///记录当前缓冲区中元素个数 Buffer1(){} Buffer1(int n){ this.n=n; buffer1=new int[n]; in=out=cnt=0; } //生产方法,加入元素x public void product(int x){ lock.lock(); //加锁 try{ if((in+1)%n==out){ //如果缓冲区满,则阻塞当前线程 con.await(); //con.signalAll(); } else{ buffer1[in]=x; in=(in+1)%n; cnt++; System.out.println(Thread.currentThread().getName()+"向缓冲区中加入元素:"+x); System.out.println("当前缓冲区中的元素个数为: "+cnt); con.signalAll(); //唤醒其它线程 } } catch(InterruptedException ex){ ex.printStackTrace(); } finally{ ///使用finally语句保证锁能正确释放 lock.unlock(); } } //消费方法,取走缓冲区中的一个元素 public int consumer(){ int x=0; lock.lock(); try{ if(in==out){ //如果缓冲区空,则阻塞当前线程 con.await(); } else{ x=buffer1[out]; System.out.println(Thread.currentThread().getName()+"消费元素: "+x); out=(out+1)%n; cnt--; System.out.println("当前元素个数为: "+cnt); con.signalAll(); //唤醒其它线程 } } catch(InterruptedException ex){ ex.printStackTrace(); } finally{ lock.unlock(); } return x; }}
package lkl1;import java.util.Random;//消费者线程public class Consumer1 extends Thread{ private Random rand=new Random(); private Buffer1 buffer1; //对应的缓冲区 Consumer1(Buffer1 buffer1){ this.buffer1=buffer1; } public void run(){ while(true){ buffer1.consumer(); try{ ///在消费者线程中加一个sleep语句,可以更好的体现线程之间的切换 sleep(50); } catch(Exception x){ x.printStackTrace(); } } }}
package lkl1;import java.util.Random;//生产者线程public class Product1 extends Thread{ private Random rand = new Random(); private Buffer1 buffer1; Product1(Buffer1 buffer1){ this.buffer1=buffer1; } public void run(){ while(true){ int x; x=rand.nextInt(100); buffer1.product(x); } }}
package lkl1;///Buffer1测试//启动一个生产者线程,两个消费者线程public class Buffer1Test { public static void main(String[] args) throws Exception{ Buffer1 buffer1 = new Buffer1(10); new Product1(buffer1).start(); new Consumer1(buffer1).start(); new Consumer1(buffer1).start(); }}
三。使用阻塞队列(BlockingQueue)来控制线程通信
Java5提供了一个BlockingQueue接口,这个接口也是属于队列的子接口,但是他主要的作用还是用来进行线程通信,而不是当成队列用。BlockingQueue的特征是:当生产者线程试图向BlockingQueue中加入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列为空,则该线程会被阻塞。
这样程序的两个线程通过交替的向BlocingQueue中放入元素,取出元素,即可很好的控制线程的通信。当然也不是所有的BlockingQueue的方法都支持阻塞操作的。
BlockingQueue提供了以下两个支持阻塞的方法:
put(E e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
take():尝试从BlockingQueue的头部取出元素,如果该队列的元素为空,则阻塞该线程。
其它的Queue对应的方法,也都是支持的,但是在上面的情况下操作,不会阻塞,而是会返回false或抛出异常。
常用的BlockingQueue的实现类有以下几种:
ArrayBlockingQueue:基于数组实现的BlockingQueue队列
LinkedBlockingQueue:基于链表实现的BlockingQueue队列
PriorityBlockingQueue:优先队列对应的阻塞队列
SynchronizedQueue:同步队列,对该队列的存取必须交替进行
因为阻塞队列本身就支持生产者消费者模式,所以用阻塞队列来实现生产者消费者问题就很简单了。
package lkl;import java.util.concurrent.BlockingQueue;///消费者线程public class Consumer extends Thread{ private BlockingQueue<String>bq; public Consumer(BlockingQueue<String> bq){ this.bq=bq; } public void run(){ while(true){ System.out.println(getName()+"消费者准备消费集合元素!"); try{ Thread.sleep(200); //尝试取出元素,如果队列以空,则线程阻塞 bq.take(); } catch(Exception ex){ ex.printStackTrace(); } System.out.println(getName()+"消费完成: "+bq); } }}
package lkl;import java.util.concurrent.BlockingQueue;//生产者线程public class Producer extends Thread{ private BlockingQueue<String>bq; public Producer(BlockingQueue bq){ this.bq=bq; } public void run(){ String[] strArr = new String[]{ "Java","Struts","Spring" }; for(int i=0;i<99999999;i++){ System.out.println(getName()+"生产者准备生产集合元素!"); try{ Thread.sleep(200); //尝试放入元素,如果队列已满,则线程会被阻塞 bq.put(strArr[i%3]); } catch(Exception ex){ ex.printStackTrace(); } System.out.println(getName()+"生产完成: "+bq); } }}
package lkl;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;public class BlockingQueueTest2 { public static void main(String[] args){ //创建一个容量为1的BlockingQueue BlockingQueue<String> bq=new ArrayBlockingQueue<>(1); //启动3个生产者线程 new Producer(bq).start(); new Producer(bq).start(); new Producer(bq).start(); //启动一个消费者线程 new Consumer(bq).start(); }}
- Thinking in Java---线程通信+三种方式实现生产者消费者问题
- java线程:三种方式实现生产者消费者问题_1
- java线程:三种方式实现生产者消费者问题_2
- java线程:三种方式实现生产者消费者问题_3
- java实现生产者消费者--三种方式
- java实现生产者消费者问题 三中实现方式
- 线程通信,生产者消费者问题(Java)
- java线程通信--生产者消费者问题
- [转] Thinking In Java 中的生产者/消费者问题
- 生产者消费者问题(thinking in java 709 )
- 生产者消费者问题之Java线程实现
- java线程通信-生产者消费者
- java实现生产者和消费者问题的几种方式
- java线程(三) 生产者消费者模式-线程同步问题
- Java线程(三):线程协作-生产者/消费者问题
- Java线程(三):线程协作-生产者/消费者问题
- Java线程(三):线程协作-生产者/消费者问题
- Java线程(三):线程协作-生产者/消费者问题
- 【codechef】Chef and the Cards(dp,推论)
- java项目转成WEB项目快捷方法
- [C++11 并发编程] 16 在期望中保存异常
- 虚拟机使用同一镜像做NLB测试失败的问题
- ural 1297
- Thinking in Java---线程通信+三种方式实现生产者消费者问题
- ecplise中tomcat插件
- 利用javadoc生成API文档
- POJ 2777 Count Color(位运算+线段树+lazy+区间更新)
- 网站前端技术学习 笔记
- allegro不规则焊盘制作
- 链栈的学习(回文数)
- 小马哥----高仿小米4 主板型号M168 拆机刷机机型图与开机界面识别图 多图展示
- 高质量编程的金玉良言——开放-封闭原则