5-java多线程-生产者消费者模型

java多线程


一、生产者消费者模型

这里采用的是最基本的生产者消费者模型,生产者生产资源,消费者消耗资源。

隐蔽知识点:生产者消费者模型一般不使用集合来模拟,这是因为集合会无限扩容,使用数组可以很好的展现效果

基本模型搭建如下:还未添加线程的同步机制

// 资源类
class Resource{

    // 定义容器用来存放数据或取数据的空间
    private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
    private int num = 1;

    // 存放数据的方法
    public void save(){
        data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
        System.out.println(Thread.currentThread().getName()+"正存放的数据为"+data[0]);
        num++;
    }

    // 获取数据的方法
    public void get(){
        System.out.println(Thread.currentThread().getName()+"正取的数据为"+data[0]);
        data[0]=null;
    }
}

// producter
class Product implements Runnable{
    // 在生产者类中提供构造方法,目的是在创建线程的时候可以明确线程将来操作的资源本身
    private Resource r;
    public Product(Resource r){
        this.r = r;
    }
    public void run(){
        while(true){
            // 无限的生产资源
            r.save();
        }
    }
}


// consumer
class Consumer implements Runnable{
    private Resource r;
    public Consumer(Resource r){
        this.r = r;
    }
    public void run(){
        while(true){
            // 无限的消耗资源
            r.get();
        }
    }
}


class ThreadDemo {

    public static void main(String[] args) throws Exception {
        // 创建生产的任务对象
        Resource r = new Resource();
        Product p = new Product(r);
        Consumer c = new Consumer(r);
        
        Thread t1 = new Thread(p);// 生产者线程
        Thread t2 = new Thread(c);// 消费者线程

        t1.start();
        t2.start();

    }
}

二、生产者消费者模型的安全问题

由于cpu会在线程间切换,导致可能出现某个线程未完全跑完代码就切换到隔壁线程了。如消费者刚修改数据为null,就切换到生产者打印结果,那么打印结果自然为生产者生产了数据null。

Thread-0正存放的数据为 null
Thread-1正取的数据为 null

解决办法:加锁。

1.同步代码块

代码如下:区别就在于创建了锁,并用synchronized这一同步代码块来为线程同步区域加了锁。save方法与get方法用的是同一把锁,可以完成同步。

当然,这个代码也是存在问题的,它描述的场景是这样的,生产者可以一直生产,每次都会替换掉之前生成的产品;消费者可以一直消费,哪怕产品是null也会消费。也即是无限生产/无限消费,显然是不合理的。

class Resource{

    // 定义容器用来存放数据或取数据的空间
    private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
    private int num = 1;

    private Object lock = new Object(); // 创建锁

    // 存放数据的方法
    public void save(){
        synchronized(lock){
            data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
            System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
            num++;
        }
    }

    // 获取数据的方法
    public void get(){
        synchronized(lock){
            System.out.println(Thread.currentThread().getName()+"正取的数据为 "+data[0]);
            data[0]=null;
        }
    }
}

三、生产者消费者模型的同步问题

也即线程间通信问题。
无限生产/消费问题想要解决,我们思考一下情景。
生产者在生产时应该确定此时没有产品;消费者消费时应该确定此时有产品。也即是加入判断,在不满足条件时使线程等待。

线程间通信使用waitnotify方法。也即是PV操作。
下列代码可以完成单生产者单消费者问题。

class Resource{

    // 定义容器用来存放数据或取数据的空间
    private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
    private int num = 1;

    private Object lock = new Object(); // 创建锁

    // 存放数据的方法
    public void save(){
        synchronized(lock){
            if(data[0]!=null){
                // 如果有数据,那么生产者应该等待消费者取走
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
            System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
            num++;
            // 生产数据后要通知消费者可以消费了
            lock.notify();
        }
    }

    // 获取数据的方法
    public void get(){
        synchronized(lock){
            if(data[0]==null){
                // 如果没有数据,那么消费者应该等待生产者生产
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"正取的数据为 "+data[0]);
            data[0]=null;
            // 消费数据后要通知生产者可以生产了
            lock.notify();
        }
    }
}

四、生产者消费者模型的多生产多消费问题

上述代码只能完成单生产者单消费者问题,一旦开辟多个生产者/消费者,那么生产者唤醒锁的时候有可能唤醒了另一个生产者,那么就再次出现了无线生产问题。
落实在代码上,是因为线程被唤醒后会直接执行if后的代码。
解决方法:将 if 改为 while。一定可以解决问题,但是也有隐患,发生了死锁,所有线程都wait了。
死锁原因:唤醒的时候唤醒了自己的同伴,由于判断条件改为了while,导致同步再次判断依然被wait,最终导致没有可以执行的线程。
解决方法:JDK5之前只能采用notifyAll方法唤醒所有线程。唤醒的同伴会再次被wait,对方线程会干活,但是这种方式效率低。更希望的结果是唤醒对方线程。
或者使用JDK5中的condition接口,完成不同的等待唤醒机制监控不同的接口。

class Resource{

    // 定义容器用来存放数据或取数据的空间
    private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
    private int num = 1;

    private Object lock = new Object(); // 创建锁

    // 存放数据的方法
    public void save(){
        synchronized(lock){
            while(data[0]!=null){
                // 如果有数据,那么生产者应该等待消费者取走
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
            System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
            num++;
            // 生产数据后要通知消费者可以消费了
            lock.notify();
        }
    }

    // 获取数据的方法
    public void get(){
		...
    }
}

五、lock+condition解决生产者消费者模型

同步问题要么使用synchronized同步代码块+notify/notifyAll来解决,要么使用lock+condition来解决,其中lock对应synchronized,condition对应notify。

如果使用lock+condition方法来解决问题,就和OS课程上学习的PV十分接近,代码如下:主要更改的地方有锁的创建、加锁去锁、PV操作。

此方法效率更高。

class Resource{

    // 定义容器用来存放数据或取数据的空间
    private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
    private int num = 1;

	// 创建锁, Lock是一个接口,ReentrantLock是该接口的实现类
    private Lock l = new ReentrantLock(); 

	// 给Lock接口下绑定多个用于监视线程的Condition对象
	private Condition proCon = l.newCondition(); // 监视生产者线程,要绑定在锁l之上
	private Condition conCon= l.newCondition(); // 监视消费者线程,要绑定在锁l之上

    // 存放数据的方法
    public void save(){
        l.lock();// 手动上锁
        try{ // 使用try-finally的写法是为了一定执行unlock方法,否则出现意外如exception,那么可以不会unlock,就出问题了
            while(data[0]!=null){
                // 如果有数据,那么生产者应该等待消费者取走
                try {
                    proCon.await();// P是await()
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
            System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
            num++;
            // 生产数据后要通知消费者可以消费了
            conCon.signal();// V是signal()
        }finally{
       		// 手动解锁
            l.unlock();
        }
    }

    // 获取数据的方法
    public void get(){
		...
    }
}

完整代码


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 资源类
class Resource{
    // 定义容器用来存放数据或取数据的空间
    private Object[] data = new Object[1]; // 方便演示所以仅开辟一个空间
    private int num = 1;

	// 创建锁, Lock是一个接口,ReentrantLock是该接口的实现类
    private Lock l = new ReentrantLock(); 

	// 给Lock接口下绑定多个用于监视线程的Condition对象
	private Condition proCon = l.newCondition(); // 监视生产者线程,要绑定在锁l之上
	private Condition conCon= l.newCondition(); // 监视消费者线程,要绑定在锁l之上

    // 存放数据的方法
    public void save(){
        l.lock();// 手动上锁
        try{ // 使用try-finally的写法是为了一定执行unlock方法,否则出现意外如exception,那么可以不会unlock,就出问题了
            while(data[0]!=null){
                // 如果有数据,那么生产者应该等待消费者取走
                try {
                    proCon.await();// P是await()
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            data[0] = "data"+num; // 正常而言应该是通过参数传入数据,这里为了演示将其写死,并通过num来体现生产的不同东西
            System.out.println(Thread.currentThread().getName()+"正存放的数据为 "+data[0]);
            num++;
            // 生产数据后要通知消费者可以消费了
            conCon.signal();// V是signal()
        }finally{
       		// 手动解锁
            l.unlock();
        }
    }

    // 获取数据的方法
    public void get(){
        l.lock();
        try{
            while(data[0]==null){
                // 如果没有数据,那么消费者应该等待生产者生产
                try {
                    conCon.await();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"正取的数据为 "+data[0]);
            data[0]=null;
            // 消费数据后要通知生产者可以生产了
            proCon.signal();
        }finally{
            l.unlock();
        }
    }
}

// producter
class Product implements Runnable{
    // 在生产者类中提供构造方法,目的是在创建线程的时候可以明确线程将来操作的资源本身
    private Resource r;
    public Product(Resource r){
        this.r = r;
    }
    public void run(){
        while(true){
            // 无限的生产资源
            r.save();
        }
    }
}


// consumer
class Consumer implements Runnable{
    private Resource r;
    public Consumer(Resource r){
        this.r = r;
    }
    public void run(){
        while(true){
            // 无限的消耗资源
            r.get();
        }
    }
}


class ThreadDemo {

    public static void main(String[] args) throws Exception {
        // 创建生产的任务对象
        Resource r = new Resource();
        Product p = new Product(r);
        Consumer c = new Consumer(r);
        
        Thread t1 = new Thread(p);// 生产者线程
        Thread t11 = new Thread(p);// 生产者线程
        Thread t2 = new Thread(c);// 消费者线程
        Thread t21 = new Thread(c);// 消费者线程

        t1.start();
        t2.start();
        t11.start();
        t21.start();

    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值