多线程整理笔记(二)生产者消费者模型

269 阅读7分钟

在多线程面试的过程中我们经常会被问到关于生产者消费者的问题,然后生产者消费者模型在工作中也是常常使用到的,即便是很多封装的库那也是常常被使用的。老规矩先抛出几个常规面试问题 1.手写代码实现生产者消费者的方式有哪些? 2.Synchronized 和 ReentrantLock 的区别 3.什么情况下会死锁? 生产者消费者模型中我们可以将一部分线程理解为工人 一部分线程理解为卡车,需要将工人生产出的产品运走(消费)。那么我们是否需要一个或者几个仓库用来存这些生产出的产品。那么我们可以快速构建出几个角色来完成这个场景,其实很这种场景很多我被问到的情况也很多,但是面试官不会直接问你使用多线程怎么解决。有问车站同时卖票的的多个窗口卖10000张票的问题。有的问,天网中快速识别一个人的面部,分为ABCD 四个工序,每个工序耗时不同,怎么解决。这些都不用怕!!多线程帮你解决!回到上面三个角色的问题三个角色分别为生产者,消费者,队列 ! 如下面流程图

我们要保证每一个生产者生产出的产品不一样,并且要消费者消费的产品不一样避免重复消费,也就是避免一张票卖给了多个人。这里就要使用到多线程的同步机制。我们先使用Synchronized 并且完成一个生产者,首先我们自己定义一个同步队列

 public class MyContainer<T> {
    private LinkedList<T> container;
    private int count = 0;
    private int MAX_SIZE = 20;
    public MyContainer(int MAX_SIZE) {
        this.container = new LinkedList<>();
        this.MAX_SIZE = MAX_SIZE;
    }
    public int getCount() {
        return count;
    }
    /**
     * 多线程调用
     */
    public void put(T t) {
        synchronized (container) {
            while (count == MAX_SIZE) {
                try {
                    container.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            container.add(t);
            ++count;
            container.notifyAll();
            System.out.println("ThreadName :" + Thread.currentThread().getName() + "--result---:" + t.toString());
        }
    }
    public T get() {
        synchronized (container) {
            while (count == 0) {
                try {
                    container.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T first = container.removeFirst();
            count--;
            System.out.println("ThreadName :" + Thread.currentThread().getName() + "--result---:" + first.toString());
            container.notifyAll();
            return first;
        }
    }
}

我们自定义一个队列,使用LinkedList 来实现的,当然linkedList 是线程不安全的,当我们生产者往队列里put(T t) 的时候这时候会去判断当前仓库是否已经满了,如果满了当前线程就会在这里阻塞,其他的生产者也不能进来,这时候wait()会释放当前实例对象的锁,其他在在锁池等待的线程等锁进来后,抢锁成功的线程会拥有执行权。但是阻塞的这个线程会一直在这里阻塞,等待其他持有该同样的锁的线程唤醒。如果没有阻塞,那么生产者会一直往队列中添加产品。这里的notifyall,同样会唤醒阻塞的生产者和阻塞的消费者。 如果生产者队列满了当然唤醒生产者也没有用所以直到唤醒消费者会去消费调用get() 方法,当然每个get 方法只允许一个线程进来,当队列为空的时候也会阻塞,即使其他消费者线程获得锁进来也会阻塞。

public class MyClass {
    public static void main(String[] args) {
        MyContainer<Bean> myContainer = new MyContainer<>(50);
        Thread thread0 = new Thread(new Productor(myContainer), "生产者A");
//        Thread thread1 = new Thread(new Productor(myContainer), "生产者B");
//        Thread thread2 = new Thread(new Productor(myContainer), "生产者C");
//        Thread thread3 = new Thread(new Productor(myContainer), "生产者D");
        Thread thread4 = new Thread(new Consumer(myContainer), "消费者1");
        Thread thread5 = new Thread(new Consumer(myContainer), "消费者2");
        thread0.start();
//        thread1.start();
//        thread2.start();
//        thread3.start();
        thread4.start();
        thread5.start();
    }

    private static class Productor implements Runnable {
        private MyContainer<Bean> myContainer;

        public Productor(MyContainer<Bean> myContainer) {
            this.myContainer = myContainer;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                Bean bean = new Bean();
                bean.setPName("产品" + i);
                bean.setLineName(Thread.currentThread().getName());
                myContainer.put(bean);
            }
        }
    }

    private static class Consumer implements Runnable {
        private MyContainer<Bean> myContainer;

        public Consumer(MyContainer<Bean> myContainer) {
            this.myContainer = myContainer;
        }

        @Override
        public void run() {
            while (true) {
                myContainer.get();
            }
        }
    }
}

好了我们来看一下结果

无论开启几个生产者消费者,那么始终的去维护一个线程安全的对列。上面说了,当生产者线程阻塞的时候,其他生产者在notifyALL的时候同样会唤醒生产者线程,及时生产者线程拿到执行权也没用,所以这里cpu 调用就做了无用功,那么我们只需要唤醒消费者可以吗? 当然可以这里我们就使用到ReentrantLock来完成同样的事情,并且性能还能提升不少,我们来修改一下这个同步队列。

/**
 * 自己定义同步队列
 */
public class MyContainer<T> {
    private LinkedList<T> container;
    private int count = 0;
    private int MAX_SIZE = 20;
    private ReentrantLock lock;
    private final Condition productor;
    private final Condition consumer;


    public MyContainer(int MAX_SIZE) {
        this.container = new LinkedList<>();
        this.MAX_SIZE = MAX_SIZE;
        lock = new ReentrantLock();
        productor = lock.newCondition();
        consumer = lock.newCondition();
    }

    public int getCount() {
        return count;
    }

    /**
     * 多线程调用
     */
    public void put(T t) {
        try {
            lock.lock();
            while (count == MAX_SIZE) {
                try {
                    productor.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            container.add(t);
            ++count;
            consumer.signalAll();
            System.out.println("ThreadName :" + Thread.currentThread().getName() + "--result---:" + t.toString());
        } finally {
            lock.unlock();
        }

    }

    public T get() {
        try {
            lock.lock();
            while (count == 0) {
                try {
                    consumer.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T first = container.removeFirst();
            count--;
            System.out.println("ThreadName :" + Thread.currentThread().getName() + "--result---:" + first.toString());
            productor.signalAll();
            return first;
        } finally {
            lock.unlock();
        }
    }
}

我们使用的重入锁的condition 因为我们可以指定条件时唤醒生产者还是唤醒消费者,而不是唤醒我们并不想唤醒的线程而浪费CPU 资源。 看到这里我们可以回答一个问题就是Synchronized 和 ReentrantLock 的区别就是 ReentrantLock 可以唤醒指定待条件的线程,当然这从性能上来讲ReentrantLock 性能会好很多,但是,这里说但是,这是JDK 1.8以前的时候,Synchronized 优化以后增加了偏向锁,轻量级锁(自旋锁) 性能好太多了,今天暂时不讲细节,第二个区别就是,Lock 可以实现公平锁,Synchronized 是非公平锁, 当然我们可以思考一下,公平锁和非公平锁谁的性能更强,谁的更弱。Lock 只能使用在方法中,而sychornized 可以在方法上修饰,也可以在方法中修饰, 还有一个就是ReentrantLock 可以实现等待可中断, 当持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免死锁的情况,通过使用lock.lockInterruptibly() 来实现这个机制。 再一个区别就是 Synchronized 在发生异常的时候会自动的去释放锁,但是ReentrantLock 是不会自动释放锁的,所以我们会在finally 中手动去释放锁。 其实生产者消费者模型还可以使用 BlockingDeque 去实现,通常的的有 LinkedBlockingDeque 和ArrayBlockingQueue 分别是无界队列和有界队列,有兴趣的朋友可以看看源码其实也是用 ReentrantLock 实现的。但是我们这里要区分一下他们共同实现的一个接口 BlockingQueue ,这里明确说一下如果要实现生产者消费者,那么要使用到Put 和take 这两个方法,因为这两个方法是线程阻塞的方法。

死锁: 我们说互斥锁,就是Synchronized 时互斥锁,ReentrantLock 也是互斥锁,当然你可以按照你的想法去实现你的互斥锁。当线程1 持有线程2 所需要资源,当线程2持有线程1 锁持有的资源,那么互相等待那么就会造成死锁。

  Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (mlock1) {
                    System.out.println("线程1.。。持有锁1.....");
                    synchronized (mLock2) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("线程1.。持有锁2.。。。。");
                    }
                    System.out.println("线程1.。释放锁 2.。。。。");
                }
                System.out.println("线程1.。释放锁 1.。。。。。");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock2) {
                    System.out.println("线程2.。持有锁2.....");
                    synchronized (mlock1) {
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("线程2.。。持有锁2.。。。。");
                    }
                    System.out.println("线程2.。。释放锁 1.。。。。");
                }
                System.out.println("线程2.。。释放锁 2.。。。。。");
            }
        });
        thread.start();
        thread2.start();

看上面两段代码我们,具体分析一下。由结果可见,线程1 先抢占锁1 紧接着,线程2 抢占锁2 。线程2 等待线程1 释放锁1 ,而线程1 等待线程2 释放锁2 进入同步代码块,所以这里就造成了死锁。 当线程互相持有对方所需要的资源时,会相互等待对方释放锁,如果线程都不主动释放所占用的资源,将产生死锁。 (空闲时候,还会是手写其他多生产者消费者模型)。