生产者消费者的Java实现
生产者:负责生产消息,在缓冲区满后休眠;
消费者:负责消费消息,在缓冲区空后休眠;
两者的休眠何时唤醒?
1.生产者休眠,是因为缓冲区满,所以只要消费者进行了消费,那么缓冲区就会有新的空间,生产者就可以继续生产,故每次消费者消费以后都要试图唤醒生产者,无论生产者是否休眠。
2.消费者休眠,是因为缓冲区空,所以只要生产者进行了生成,那么缓冲区就会有新的消息,消费者就可以继续生产,故每次生产者生成以后都要试图唤醒消费者,无论消费者是否休眠。
使用wait()和notifyAll()实现
1.wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。
一、如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。
二、如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。
三、如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。
其中wait方法有三个overload方法:
wait()
wait(long)
wait(long,int)
wait方法通过参数可以指定等待的时长。如果没有指定参数,默认一直等待直到被通知。
《最简实例说明wait、notify、notifyAll的使用方法》http://www.cnphp6.com/archives/62258
2.synchronized:当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
五、以上规则对其它对象锁同样适用.
《java synchronized详解》http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html
缓冲区
import java.util.LinkedList;
public class Storage {
private final int MAX_SIZE = 100; //此处为设置缓冲区大小
//消息的容器类,也可以选择队列这种先入先出的数据结构
private LinkedList<String> list = new LinkedList<String>();
//生产方法:由生产者调用
public void produce(String str){
//阻塞其他对list同步块的访问
synchronized(list){
//如果容器的大小等于预设的缓冲区大小,则为满,此处用while意味着被唤醒后需要再次检查
while(list.size()==MAX_SIZE){
try {
System.out.println("满");
//当前生产者线程交出list控制权,并进入等待
list.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//解除锁定以后继续执行
list.push(str);
System.out.println(Thread.currentThread().getName()+"生产:"+str);
//通知所有在等待list权限的线程继续运行(唤醒消费者)此处有疑问?下方讨论
list.notifyAll();
}
}
//消费方法:由消费者调用
public void consume(){
//阻塞其他对list同步块的访问
synchronized(list){
//如果缓冲区为空,此处用while意味着被唤醒后需要再次检查
while(list.size()==0){
try {
System.out.println("空");
//当前消费者交出list的权限,进入等待
list.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//解除锁定以后继续执行
String a = list.pop();
System.out.println(Thread.currentThread().getName()+"消费:"+a);
//通知所有在等待list权限的线程继续运行(唤醒生产者)此处有疑问?下方讨论
list.notifyAll();
}
}
}
}
生产者
import java.util.Random;
public class Producer extends Thread {
private Storage storage;
//生产者与消费者通过缓冲区发生联系
public Producer(Storage storage){
this.storage = storage;
}
public void run(){
while(true){//此处为无限生产,现实的情况可能有所不同,生产一个随机数
Random a = new Random();
storage.produce(a.nextInt(1000000)+"");
}
}
}
消费者
public class Consumer extends Thread {
private Storage storage;
//生产者与消费者通过缓冲区发生联系
public Consumer(Storage storage){
this.storage = storage;
}
public void run(){
while(true){//此处为无限消费,现实的情况可能有所不同
storage.consume();
}
}
}
Main方法
public class Main {
public static void main(String[] args) {
Storage storage = new Storage();
Producer p1 = new Producer(storage);
Producer p2 = new Producer(storage);
Producer p3 = new Producer(storage);
Producer p4 = new Producer(storage);
Consumer c1 = new Consumer(storage);
Consumer c2 = new Consumer(storage);
p1.start();
c1.start();
p2.start();
c2.start();
p3.start();
p4.start();
}
}
运行结果
一个疑问
上面生产者与消费者的一个运行状态是
Producer | Consumer |
---|---|
满,进入等待,每次生成唤醒Consumer | 空,进入等待,每次消费唤醒Producer |
我的疑问:缓冲区满,生产者进入等待,没毛病,因为继续生产也会被丢弃,和网络丢包一样。但是当缓冲区为空时,消费者为什么要进入等待呢,消费者为不发送一个notifyAll然后继续自旋?
public void produce(String str){
synchronized(list){
while(list.size()==MAX_SIZE){
try {
System.out.println("满");
list.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
list.push(str);
System.out.println(Thread.currentThread().getName()+"生产:"+str);
}
}
public void consume(){
synchronized(list){
while(list.size()==0){//不会释放锁
System.out.println("空");
list.notifyAll();
}
String a = list.pop();
System.out.println(Thread.currentThread().getName()+"消费:"+a);
list.notifyAll();
}
}
一点分析:对比之前的方案,方案二可以理解为,生产者在库存满了之后会进入等待,消费者要做的就是买买买,如果售罄了,消费者不停的要求补货(不等待)….这样写也是线程安全的。
方案二的问题:消费者在缓冲区为空时候的会一直自旋不等待,浪费资源,而且过多的空消费在现实中可能被认定为错误(比如为空的时候pop())。还有就是一直自旋的话是不会释放锁的(T_T)。正如售罄时候,我们希望消费者试了一次发现售罄了,通知商家,然后回家等通知。
结论:消费者休眠为的是减少无效的消费,同理,生产者休眠是为了不丢弃资源(这个好理解一点)。
后来查证规定就是消费者不能空消费………..但是并不妨碍我们思考一波
使用await() / signal()实现
此处使用了ReentrantLock与Condition。
就单从效果上看,可以认为线程在调用lock()的时候,如果锁空闲,则获得锁,否则阻塞。
而Condition提供了更加灵活的等待操作,可以提供多个条件控制,而且具有锁的公平性。
上一个方法中,缓冲区满与缓冲区空都是用list.wait()分别进行的等待操作。
所以有没有可能list.notifyAll()同时唤醒Producer与Consumer呢?
答案:不可能,list.notifyAll()既可以唤醒Producer又可以唤醒Consumer。But,Producer与Consumer不可能同时进入wait()状态,他们俩的状态是互斥的。
由于在生产者消费者中,缓冲区只有两种临界状态:满或空。试想如果有多种状态时,如果还用自带的 wait与notify来进行唤醒,可能会发生错误的唤醒。
所Condition提供了更加精确的线程间通信。
《ReentrantLock与Condition》 http://www.tuicool.com/articles/6vANna
注意:把unlock放到finally里面以防止异常时解锁失败,判断条件用while,因为解锁以后需要再次确认状态以防止错误解锁。
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Storage {
private final int MAX_SIZE = 100;
private LinkedList<String> list = new LinkedList<String>();
private final Lock lock = new ReentrantLock();
private final Condition full = lock.newCondition();
private final Condition empty = lock.newCondition();
public void produce(String str){
lock.lock();
try {
while(list.size()>=MAX_SIZE){
System.out.println("满");
full.await();
}
list.push(str);
System.out.println(Thread.currentThread().getName()+"生产:"+str);
empty.signalAll();
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void consume(){
lock.lock();
try {
while(list.size()<1){
System.out.println("空");
empty.await();
}
String a = list.poll();
System.out.println(Thread.currentThread().getName()+"消费:"+a);
full.signalAll();
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
不用while而用if进行判断的结果。
在这个例子中体现不出来。因为生产者都是生产1个,消费者都是消费1个,试想消费者消费多个,生产者生成多个的时候,如果用if,那么线程被Notify唤醒后会直接进行生产或者消费。但是我们并不知道此时条件是否合适。
比如消费者C需要100个资源,而缓冲区只有10个,现在生产者P生产了10个资源,并执行signal或者Notify。那么对需消费者C来说现有的20个资源还是不够的,他应该再次进入等待,所以应该使用while来保证每次唤醒后自检。
如果消费者与生产者的工作效率是一样的(生产率=消费率),且条件只有一个互斥条件时,可以用if,不过为了保证安全还是加上吧。
使用BlockingQueue实现
使用Blockingqueue就比较简单了,因为BlockingQueue是一个线程安全的容器,同步都已经在容器内部实现了,我们只需要使用就可以了。
上面我们所写的Storage其实可以看做是一个线程安全的容器了,BlockingQueue提供的功能更多。
(挖个坑,之后详细讲BlockingQueue)
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Storage3 {
private static ArrayBlockingQueue<String> buffer = new ArrayBlockingQueue<String>(100);
private static class Producer implements Runnable{
@Override
public void run() {
while(true){
try {
buffer.put("data");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
private static class Consumer implements Runnable{
@Override
public void run() {
while(true){
try {
buffer.take();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Producer());
executor.execute(new Consumer());
executor.shutdown();
}
}
使用PipedInputStream / PipedOutputStream实现(之后讨论)
使用管道通信时,大致的流程是:我们在线程A中向PipedOutputStream中写入数据,这些数据会自动的发送到与PipedOutputStream对应的PipedInputStream中,进而存储在PipedInputStream的缓冲中;此时,线程B通过读取PipedInputStream中的数据。就可以实现,线程A和线程B的通信。
《PipedOutputStream和PipedInputStream简介》 http://www.cnblogs.com/skywang12345/p/io_04.html
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class PipeConsumeProduce {
private static PipedInputStream input = new PipedInputStream();
private static PipedOutputStream output = new PipedOutputStream();
private static class Producer implements Runnable{
private static AtomicInteger i = new AtomicInteger(1);
@Override
public void run() {
try {
input.connect(output);
while(true){
int a = i.getAndIncrement();
output.write(a);;
output.flush();
//由于此处写的是byte所以会溢出变小
System.out.println(Thread.currentThread().getName()+"生产:"+a%255);
}
}catch (IOException e) {
e.printStackTrace();
}finally{
try {
input.close();
output.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
private static class Consumer implements Runnable{
@Override
public void run() {
try {
while(true){
int a = input.read();
System.out.println(Thread.currentThread().getName()+"消费:"+a);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}catch (IOException e) {
e.printStackTrace();
}finally{
try {
input.close();
output.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Producer());
executor.execute(new Consumer());
executor.shutdown();
}
}
参考:
http://blog.youkuaiyun.com/monkey_d_meng/article/details/6251879