美文网首页
多线程高并发编程之生产者消费者面试题

多线程高并发编程之生产者消费者面试题

作者: Minority | 来源:发表于2020-03-05 17:05 被阅读0次

面试题:
写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持两个生产者线程以及10个消费者线程的阻塞调用

Case1(使用wait和nofify实现):

package ProductorAndConsumer;

import com.sun.xml.internal.bind.v2.runtime.output.Pcdata;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

public class PC1 <T>{
    //并不进行查找,所以使用链表的效率高
    final private LinkedList<T> lists = new LinkedList<>();
    final private int Max = 10;
    private int count = 0;

    public synchronized void put(T t){

        while (lists.size() == Max){
            try {
                /*while 99%都是和wait一起用,而不是if
                *
                * 原因是:wait是会释放锁的,被唤醒之后如果是if,则只执行一遍循环,接下来会执行add,这时候,
                * 其他的线程可能也add,会造成溢出。因为是notifyall,并且wait醒来得到this的锁可以运行时,就直接执行add啦。
                * */
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

        //如果不满10个,则可以添加
        lists.add(t);
        ++count;

        //通知消费者进行消费(现在是叫醒所有阻塞在this上的线程)
        //注意:不能使用notify,只能使用notifyAll,因为使用notifyA可能叫醒的又是一个生产者,因为生产者和消费者都是阻塞在this这一个对象上。
        //如果叫醒的还是生产者,就死锁啦,注意,永远要使用notifyAll,而不要使用notify
        this.notifyAll();

    }

    public synchronized T get(){
        T t = null;

        //消费者在没有product的时候阻塞
        while (lists.size() == 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

        //如果不等于0则可以进行消费
        t = lists.removeFirst();
        count--;

        //通知生产者进行生产,注意:不能使用notify,只能使用notifyAll,道理同上
        this.notifyAll();

        return t;
    }

    public static void main(String[] args) {

        //泛型实例化为String类型的对象
        PC1<String> c = new PC1<>();

        //启动消费者线程
        for (int i = 0; i<10; i++){
            //启动10个消费者线程
            new Thread(()->{

                //每个消费者线程消费5个
                for (int j = 0; j < 5; j++) {
                    System.out.println(c.get());
                }


            }, "c"+i).start();
        }


        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //启动2个生产者线程
        for (int i = 0; i < 2; i++) {
            new Thread(()->{

                //每个生产者线程生产25个
                for (int j = 0; j < 25; j++) {
                    c.put(Thread.currentThread().getName()+" "+j);
                }
            },"p"+i).start();
        }
    }
}

上面代码是使用wait和nofify实现的生产者、消费者模式。先启动消费者线程,再启动生产者线程。消费者10个线程,每次消费5个,生产者2个线程,每次生产25个。主要注意的有以下几点:

注意:

  • 需要使用一个容器来缓存消费者来不及消费的,这个容器使用的是LinkedList,因为容器中并不进行查找,所以使用链表的效率高
  • while 99%都是和wait一起用,而不是if
    原因是:在上面的判断满的操作中,wait是会释放锁的,被唤醒之后如果是if,而这时size = Max,醒来之后不会再进行判断size是否等于Max而直接进行add操作,就会造成容器的溢出。
  • 生产者和消费者锁定同一个信号量时,不能使用notify,只能使用notifyAll
    因为如果生产者使用notify可能叫醒的又是一个生产者,因为生产者和消费者都是阻塞在this这一个对象上。这样就产生了死锁。注意,永远要使用notifyAll,而不要使用notify

Case2(使用Lock和Condition双信号量实现):

package ProductorAndConsumer;

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


public class PC2<T> {

    final private LinkedList<T> lists=new LinkedList<T>();
    final private int MAX=10;   //最多10个元素
    private int count=0;

    private Lock lock=new ReentrantLock();
    //producer和consumer就是声明的两个信号量(Condition)
    //可以精确的通知是消费者被叫醒还是生产者被叫醒
    private Condition producer=lock.newCondition();
    private Condition consumer=lock.newCondition();


    public void put(T t){

        lock.lock();
        try {
            while (lists.size()==MAX){
                //注意:调用的是await,不要调wait。wait是和synchronized一起使用的
                //await和wait功能上是没啥区别的。只是API调用不同
                //lock和await、signalAll一起使用。synchronized和wait、notifyAll一起使用
                producer.await();
            }
            lists.add(t);
            ++count;
            consumer.signalAll();//只通知消费者线程进行消费
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public T get(){
        T t=null;
        lock.lock();
        try {
            while (lists.size()==0){
                consumer.await();
            }
            t=lists.removeFirst();
            count--;
            producer.signalAll();   //只通知生产者进行生产
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }

    public static void main(String[] args) {
        PC2<String> c =new PC2<>();



        //启动消费者线程
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 5; j++) {
                    System.out.println(c.get());
                }
            },"c"+i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //启动生产者线程
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                for (int j = 0; j < 25; j++) {
                    c.put(Thread.currentThread().getName()+" "+j);
                }
            },"p"+i).start();
        }
    }
}

这里并未使用synchronized,而是使用的ReentrantLock。然后通过ReentrantLock创建两个Condition信号量。第一种方法只能使用notifyAll,而不能使用notify使用为生产者和消费者对同一个对象进行加锁,所以只能全部通知,而不能精确的指定到底是生产者线程被唤醒还是消费者线程被唤醒。

本例中使用Lock和Condition来实现,与第一种方式对比,condition的方式可以更加精确的指定哪些线程被唤醒。producer和consumer就是声明的两个信号量(Condition),可以精确的通知是消费者被叫醒还是生产者被叫醒

注意点:

  • ReentrantLock是手动锁,必须手动进行释放锁,详见这篇文章
  • ReentrantLock自己创建了一个对象锁lock,lock和await、signalAll一起使用。synchronized和wait、notifyAll一起使用。await和wait功能上是没啥区别的。只是API调用不同
  • condition的方式可以更加精确的指定哪些线程(生产者or消费者)被唤醒



参考:马士兵老师java多线程高并发编程

相关文章

网友评论

      本文标题:多线程高并发编程之生产者消费者面试题

      本文链接:https://www.haomeiwen.com/subject/rvnfrhtx.html