Java高并发编程之经典面试题:生产者与消费者线程

面试题如下:
写一个固定容量的同步容器,拥有put、get和getCount方法,要求能够支持5个生产者线程以及10个消费者线程的阻塞调用。
写一个容器拥有put、get和getCount,这事不难,难点在于,这是一个同步容器,就是说当有多个线程同时进行put和get的时候,不能出错。
进一步分析:
1、如果容器为空,那么调用get方法的线程(消费者)需要等待(wait)
2、如果容器已满,那么调用put方法的线程(生产者)需要等待(wait)
3、当容器不为空,通知所有消费者进行get操作(notify/notifyAll)
4、当容器还未满,通知所有生产者进行put操作(notify/notifyAll)
5、多生产与多消费者之间需要同步调用(synchronized)
实现这个阻塞式的同步容器,首先想到的是、使用synchronized和wait、notify/notifyAll结合实现同步和线程间通讯。
分析了思路后,实现起来其实非常简单,代码如下:

public class MyContainer1 {
    final private LinkedList lists = new LinkedList<>();
    /**
     * 容器固定容量为10
     */
    final private int MAX = 10;
    /**
     * 容器初始元素个数为0
     */
    private int count = 0;

    /**
     * put方法
     * @param t
     */
    public synchronized void put(T t) {
        //注意这里使用while而不是if
        while(MAX == lists.size()) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        lists.add(t);
        ++count;

        System.out.println(Thread.currentThread().getName() + "进行生产了,当前剩余:" + getCount());

        //通知所有消费者线程进行消费
        this.notifyAll();
    }

    /**
     * get 方法
     * @return
     */
    public synchronized T get() {
        while (0 == lists.size()) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        T t = lists.removeLast();
        --count;

        System.out.println(Thread.currentThread().getName() + "进行消费了,当前剩余:" + getCount());

        //通知所有生产者线程
        this.notifyAll();
        return t;
    }

    /**
     * getCount方法
     * @return
     */
    public synchronized int getCount() {
        return this.count;
    }

    public static void main(String[] args) {
        MyContainer1 myContainer1 = new MyContainer1<>();
 
        for(int i = 1; i <= 5; i++) {
            new Thread(()->{
                while(true) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        myContainer1.put("");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "p_thread_" + i).start();
        }

        for(int i = 1; i <= 10; i++) {
            new Thread(()->{
                while(true) {
                    try {
                        TimeUnit.SECONDS.sleep(3);
                        myContainer1.get();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "c_thread_" + i).start();
        }
    }
}

有个小陷阱需要特别注意,put和get方法里面判断容器容量的时候用的while而不是if,这是为什么呢?
假如现在用的是if,某个时刻出现如下状况:
1、容器已满
2、5个生产者线程轮流执行put方法,if语句判断容器已满,5个生产者依次进入wait转态
3、接着某个消费者线程执行了get方法,并唤醒所有wait状态的生产者线程
4、所有生产者线程依次执行wait后面的语句,即lists.add(t)++count;
问题出在第4步,因为用的是if,所以每个线程都不会再次进行判断就执行wait后面的语句了,这就将导致容器溢出!get方法也是同样的道理。
记住这个比较高级的编程技巧:用到wait的时候,99.9%的情况下,都需要同while一起使用。

你可能感兴趣的:(Java开发)