Java多线程—生产者与消费者模型

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,如果先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。

再看此模型之前,还得先看几个重要的方法:

1. wait()与notify()方法

1.1 wait()方法

方法 wait() 的作用是使当前执行代码的线程进行等待,wait() 方法是 Object 类的方法,该方法用来将当前线程置入“预执行队列”中,并且在 wait() 所在的代码处停止执行,直到接到通知或被中断为止。在调用 wait() 方法之前,线程必须先获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait() 方法。在执行 wait() 方法后,当前线程释放锁。在从 wait() 返回前,线程与其他线程竞争重新获得锁。如果调用 wait() 时没有持有适当的锁,则 wait() 返回前,线程与其他线程竞争重新获得锁。如果调用 wait() 时没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch语句进行捕捉异常。

范例:wait()方法的使用:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("等待中...");
            object.wait();
            System.out.println("等待结束...");
        }
        System.out.println("main方法结束...");
    }
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到
了另外一个方法唤醒的方法notify()。

1.2 notify()方法
方法 notify() 也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用 notify() 时没有持有适当的锁,也会抛出IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈 wait 状态的线程,对其发出通知 notify ,并使它等待获取该对象的对象锁。需要说明的是,在执行 notify() 方法后,当前线程不会马上释放该对象锁,呈 wait 状态的线程也并不能马上获取该对象锁.要等到执行 notify() 方法的线程将程序执行完,也就是退出 synchronized 代码块后,当前线程才会释放锁,而呈 wait 状态所在的线程才可以获取该对象锁。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,还会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或notifyAll。

范例:使用notify()方法唤醒线程:

class MyThread implements Runnable {
    private boolean flag;
    private Object obj;
    public MyThread(boolean flag, Object obj) {
        super();
        this.flag = flag;
        this.obj = obj;
    }

    public void waitMethod() {
        synchronized (obj) {
            try {
                while (true) {
                    System.out.println("wait()方法开始.."+Thread.currentThread().getName());
                    obj.wait();
                    System.out.println("wait()方法结束.."+Thread.currentThread().getName());
                    return;
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void notifyMethod() {
        synchronized (obj) {
            try {
                System.out.println("notify()方法开始..."+Thread.currentThread().getName());
                obj.notify();
                System.out.println("notify()方法结束..."+Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        if (flag) {
            this.waitMethod();
        } else {
            this.notifyMethod();
        }

    }

}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        MyThread waitThread = new MyThread(true, object);
        MyThread notifyThread = new MyThread(false, object);
        Thread thread1 = new Thread(waitThread, "wait线程");
        Thread thread2 = new Thread(notifyThread, "notify线程");
        thread1.start();
        Thread.sleep(1000);
        thread2.start();
        System.out.println("main方法结束...");
    }
}

从结果上来看第一个线程执行的是一个waitMethod方法,该方法里面有个死循环并且使用了wait方法进入等待状
态将释放锁,如果这个线程不被唤醒的话将会一直等待下去,这个时候第二个线程执行的是notifyMethod方法,
该方法里面执行了一个唤醒线程的操作,并且一直将notify的同步代码块执行完毕之后才会释放锁然后继续执行
wait结束打印语句。
需要注意的是:wait,notify必须使用在synchronized同步方法或者代码块内。

将上面的Test类修改一下:

// 尝试使用notify()方法对多个wait线程唤醒
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        MyThread waitThread1 = new MyThread(true, object);
        MyThread waitThread2 = new MyThread(true, object);
        MyThread waitThread3 = new MyThread(true, object);
        MyThread notifyThread = new MyThread(false, object);
        Thread thread1 = new Thread(waitThread1, "wait线程A");
        Thread thread2 = new Thread(waitThread2, "wait线程B");
        Thread thread3 = new Thread(waitThread3, "wait线程C");
        Thread thread4 = new Thread(notifyThread, "notify线程");
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(1000);
        thread4.start();
        System.out.println("main方法结束...");
    }
}

结果如下图:
这里写图片描述
这里写图片描述
从结果可以看出,如果有多个 wait 状态线程,那么 notify() 方法会随机挑选一个唤醒

1.3 notifyAll()方法
以上讲解了notify方法只是唤醒某一个等待线程,那么如果有多个线程都在等待中怎么办呢,这个时候就可以使用
notifyAll方法可以一次唤醒所有的等待线程,看示例。
使用notifyAll()方法唤醒所有等待线程:

class MyThread implements Runnable {
    private boolean flag;
    private Object obj;
    public MyThread(boolean flag, Object obj) {
        super();
        this.flag = flag;
        this.obj = obj;
    }

    public void waitMethod() {
        synchronized (obj) {
            try {
                while (true) {
                    System.out.println("wait()方法开始.."+Thread.currentThread().getName());
                    obj.wait();
                    System.out.println("wait()方法结束.."+Thread.currentThread().getName());
                    return;
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public void notifyMethod() {
        synchronized (obj) {
            try {
                System.out.println("notifyAll()方法开始..."+Thread.currentThread().getName());
                obj.notifyAll();
                System.out.println("notifyAll()方法结束..."+Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        if (flag) {
            this.waitMethod();
        } else {
            this.notifyMethod();
        }
    }

}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        MyThread waitThread1 = new MyThread(true, object);
        MyThread waitThread2 = new MyThread(true, object);
        MyThread waitThread3 = new MyThread(true, object);
        MyThread notifyThread = new MyThread(false, object);
        Thread thread1 = new Thread(waitThread1, "wait线程A");
        Thread thread2 = new Thread(waitThread2, "wait线程B");
        Thread thread3 = new Thread(waitThread3, "wait线程C");
        Thread thread4 = new Thread(notifyThread, "notify线程");
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(1000);
        thread4.start();
        System.out.println("main方法结束...");
    }
}

从运行结果可以看出notifyAll确实是唤醒了所有线程了。

需要主义的是:唤醒线程不能过早,如果在还没有线程在等待中时,过早的唤醒线程,这个时候就会出现先唤醒,在等待的效果了。这样就没有必要在去运行wait方法了。

用一句话来总结一下 wait 和 notify : wait使线程停止运行,而notify使停止的线程继续运行。

1.4 小结:
出现阻塞的情况大体可以分为如下5种:
1. 线程调用 sleep方法,主动放弃占用的处理器资源。
2. 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。
3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
4. 线程等待某个通知。
5. 程序调用了 suspend方法将该线程挂起。此方法容易导致死锁,尽量避免使用该方法。

run()方法运行结束后进入销毁阶段,整个线程执行完毕。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被
阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待CPU的调度;反之,一个线程被wait后,就会进入阻塞队列,等
待下一次被唤醒。

2. 生产者与消费者模型

上面几个重要方法已经介绍完毕,现在来开始学习生产者与消费者模式了。
生产者与消费者开头已经介绍过了,生产者与消费者一般需要第三者来解耦的,所以现在就模拟一个简单的商品的
生产者与消费者,由生产者线程生产出一个商品之后将由消费者线程开始消费!

首先创建一个商品类Goods,类中有商品库存以及生产&消费方法。
商品类Goods:

class Goods {
    // 商品名称
    private String goodsName;
    // 商品库存
    private int count;

    // 生产方法
    public synchronized void set(String goodsName) {
        this.goodsName = goodsName;
        this.count = count + 1;
        System.out.println(toString());
    }

    // 消费方法
    public synchronized void get() {
        // 每次消费一个商品
        this.count = this.count - 1;
        System.out.println(toString());
    }

    @Override
    public String toString() {
        return "Goods [goodsName=" + goodsName + ", count=" + count + "]";
    }
}

然后创建生产者和消费者类:

// 生产者类
class Producer implements Runnable {
    private Goods goods;

    public Producer(Goods goods) {
        super();
        this.goods = goods;
    }

    @Override
    public void run() {
        this.goods.set("保时捷911一辆");
    }
}

// 消费者类
class Consumer implements Runnable {
    private Goods goods;

    public Consumer(Goods goods) {
        super();
        this.goods = goods;
    }

    @Override
    public void run() {
        this.goods.get();   
    }
}

测试类:

public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        Goods goods = new Goods();
        Thread produceThread = new Thread(new Producer(goods), "生产者线程");
        Thread consumerThread = new Thread(new Consumer(goods), "消费者线程");
        produceThread.start();
        Thread.sleep(1000);
        consumerThread.start();
    }
}

那么现在换一种方式,将生产者线程开启和消费者线程开启的代码换个位置在再测试下。此时问题产生了,
生产者还没生产商品消费者就消费了导致数量不正确。此时就需要我们的wait()和notify()方法帮忙。
修改上述代码:

class Goods {
    // 商品名称
    private String goodsName;
    // 商品库存
    private int count;

    // 生产方法
    public synchronized void set(String goodsName) throws InterruptedException {
        if(this.count > 0) {
            System.out.println("还有库存,等待消费者...");
            wait();
        }
        this.goodsName = goodsName;
        this.count = count + 1;
        Thread.sleep(1000);
        System.out.println("生产:"+toString());
    }

    // 消费方法
    public synchronized void get() throws InterruptedException {
        if(this.count == 0) {
            System.out.println("商品卖完了,等待生产...");
            wait();
        }
        // 每次消费一个商品
        this.count = this.count - 1;    
        Thread.sleep(1000);
        System.out.println("消费:"+toString());
        // 消费完告知生产者线程可以继续生产了
        notify();
    }

    @Override
    public String toString() {
        return "Goods [商品名称为:" + goodsName + ",库存为:" + count + "]";
    }
}


// 生产者类
class Producer implements Runnable {
    private Goods goods;

    public Producer(Goods goods) {
        super();
        this.goods = goods;
    }

    @Override
    public void run() {
        try {
            this.goods.set("保时捷911");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 消费者类
class Consumer implements Runnable {
    private Goods goods;

    public Consumer(Goods goods) {
        super();
        this.goods = goods;
    }

    @Override
    public void run() {
        try {
            this.goods.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }   
    }
}

public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        Goods goods = new Goods();
        Thread produceThread = new Thread(new Producer(goods), "生产者线程");
        Thread consumerThread = new Thread(new Consumer(goods), "消费者线程");
        consumerThread.start(); 
        produceThread.start();
    }
}

3. 多生产以及多消费

以上只有一个生产者生产一次商品和一个消费者只消费一次就结束了,现在能否改变一下,多个生产者和多个消费
者呢?这样的话我们该怎么改造代码呢?
分析一下,首先notify方法目前是只能唤醒一个线程,如果有多个生产者线程和多个消费者线程的话,这个notify方法唤醒的线程如果是消费者的话应该没有问题,但是如果是唤醒的也是生产者的线程那么程序就会变成假死状态了,这个时候显然这个notify方法不行,之前讲过了有一个notifyAll()唤醒当前对象的所有线程。这个时候就可以使用该方法开始改造。

改造多生产:

// 生产方法
public synchronized void set(String goodsName) throws InterruptedException {
    if(this.count > 0) {
        wait();
    }
    this.goodsName = goodsName;
    this.count = count + 1;
    Thread.sleep(1000);
    System.out.println(Thread.currentThread().getName());
    System.out.println("生产:"+toString());
    System.out.println("====================");
    notifyAll();
}

@Override
public void run() {
    while(true) {
        try {
            this.goods.set("保时捷911");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

改造多消费:

public synchronized void get() throws InterruptedException {
    if(this.count == 0) {
        wait();
    }
    // 每次消费一个商品
    this.count = this.count - 1;    
    Thread.sleep(1000);
    System.out.println(Thread.currentThread().getName());
    System.out.println("消费:"+toString());
    System.out.println("=========================");
    // 消费完告知生产者线程可以继续生产了
    notifyAll();
}

@Override
public void run() {
    while (true) {
        try {
            this.goods.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }   
}

修改测试类引入多个生产、消费过程:

public class TestDemo {
    public static void main(String[] args) throws InterruptedException {
        Goods goods = new Goods();
        List<Thread> threadList = new ArrayList<>();

        for(int i = 0; i < 10; i++) {
            Thread produceThread = new Thread(new Producer(goods));
            produceThread.setName("生产者线程 "+i);
            threadList.add(produceThread);
        }
        for (int i = 0; i < 6; i++) {
            Thread consumeThread = new Thread(new Consumer(goods));
            consumeThread.setName("消费者线程 "+i);
            threadList.add(consumeThread);
        }
        for (Thread thread : threadList) {
            thread.start();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/yubujian_l/article/details/80211637