05多线程之生产者与消费者详解

2020/5/5

介绍

生产者与消费者就是程序中有两大类线程生产者消费者这两大类,生产者生产的资源数据供消费者消费。一般情况下,有一块共享内存,生产者向这块内存中生产,消费者从中取出数据进行消费

  • 关键要协调生产线程与消费线程,一个时刻只能有一个线程访问资源(这就要确保各个线程所有线程互斥)
  • 此外,生产者线程与消费者线程之间要进行通信,采取什么样的通信机制呢?何时唤醒何时阻塞

05多线程之生产者与消费者详解_第1张图片

在Java中BlockingQueue阻塞队列中使用到了生产者与消费者模式,我们先自己实现一个生产者与消费者模式,之后再分析BlocingQueue源码

我们举的例子为,多个线程对一个数进行加与减操作,确保这个数的值为0或1之间变换,可以采用原生的synchronized+notifyreebtranLock+signall+await两种方式,其对应的代码如下:

Synchronized版本生产者消费者

package Multithread.Test;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/5/5 0005  14:22
 * 生产者 消费者模式
 *  要保证两点:
 *  1、各个线程之间要同步----》即一个时刻只能有一个线程访问共享资源(因此要加锁)
 *  2、生产者线程与消费者线程要能实现通信
 *  两个线程交替执行
 *      A--B
 */
public class Consumer01 {

    public static void main(String[] args) {
        Data data = new Data();
        new Thread(()->{while (true)data.add();},"A").start();
        new Thread(()->{while (true)data.decry();},"B").start();
        new Thread(()->{while (true)data.decry();},"C").start();
        new Thread(()->{while (true)data.decry();},"D").start();

    }
}


//共享资源
//在多线程并发中首先要找到 共享资源
class Data{

    //多个线程对这个数据进行操作
    //+1 -1的操作
    private int num = 0;

    //加1的操作

    public synchronized  void add(){
        //假如该值为1则需要进行等待
        //将if改成while避免虚假唤醒
        while (num==1){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //否则,将值+1
        System.out.println(Thread.currentThread().getName()+"进行加操作职位"+(num++));
        // Wakes up all threads that are waiting on this object's monitor.
        //唤起所有在等待这个监视器的线程(这里就是等待data的监视器)
        notifyAll();
    }



    public synchronized void decry(){

        while (num==0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(Thread.currentThread().getName()+"进行减法操作"+(num--));
        //唤起所有等待这个监视器的线程
        notifyAll();
    }



}


reentranLock版本生产者与消费者


package Multithread.Test;

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

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/5/5 0005  14:50
 * 
 */
public class Consumer02 {

    public static void main(String[] args) {

        Data2 data2 = new Data2();

        new Thread(()->{while (true) data2.add();},"A").start();
        new Thread(()->{while (true) data2.decry();},"b").start();

    }

}

class Data2{

    private int number = 0;
    //定义可重入锁
    private ReentrantLock lock= new ReentrantLock();
    //监视器
    private Condition condition = lock.newCondition();

    public void add(){
        lock.lock();
        try {
            //等待
            while (number==1){
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //
            System.out.println(Thread.currentThread().getName()+"进行了加"+(++number));
            //唤醒等待在这个监事器上的线程
            condition.signalAll();
        }finally {
            lock.unlock();
        }

    }

    public void decry(){

        lock.lock();
        try{
            while (number==0){//一定要加while防止虚假唤醒
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"的找为"+(--number));
            condition.signalAll();
        }finally {
            //解锁
            lock.unlock();
        }

    }

}

注意在进行条件等待的时候,必须使用while条件进行判断,这个是在jdk源码中说明了的,防止虚假唤醒

ArrayBlockingQueue源码分析

ArrayBlockingQueue中使用到了生产者与消费者模式,比如说底层的数组满了的话,若数组已经满了,则向数组中添加数据的线程则不能添加数据,直到数组中有空的位置;若数组中没有数据了,则不能从数组中取数据,直到数组中有了数据,才可以通知取数据线程从数组中取出数据。底层用的是ReetranLock
注意:在ArrayBlockingQueue底层有多组API,有的可以阻塞队列,有的不会阻塞队列,在这里我们分析的是put与take

  • 底层用的是一把注意与Arrayblicking的区别

测试

package Multithread.Test;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/5/9 0009  22:43
 */
public class TestArrayBlockingQueue {

    public static void main(String[] args) throws InterruptedException {

        //阻塞队列的容量为3
        BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(3);

       blockingQueue.put(1);
       blockingQueue.put(2);
       blockingQueue.put(3);

       //从阻塞队列中取出数据,取5个数据,最后肯定会一直阻塞,因为我们只向阻塞队列中添加了4个数据
       new Thread(()->{for(int i=0;i<5;i++) {
           try {
               Thread.sleep(1000);
               System.out.println(blockingQueue.take());
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       }).start();

       //此时阻塞队列已经满了,进程会阻塞,
       blockingQueue.put(4);



    }
}

底层数据


  /** The queued items */
    //共享内存
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

    /** Number of elements in the queue */
    int count;

生产者put()方法分析

   /**
     * Inserts the specified element at the tail of this queue, waiting
     * for space to become available if the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //尝试加锁
        lock.lockInterruptibly();
        try {
            //若数组已经满了,在等待不满通知.这里采用的就是while循环 避免虚假唤醒
            while (count == items.length)
                notFull.await();
            //调用一个封装的方法
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    //////////////////////////
    ////////////////////////
     /**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        //对数组添加元素
        items[putIndex] = x;
        //循环数组,当数组满了
        if (++putIndex == items.length)
            putIndex = 0;
        //计算数组中的元素怒
        count++;
        //通知 数组中有数据了,可以进行取值了
        notEmpty.signal();
    }


消费者take()

 public E take() throws InterruptedException {

        final ReentrantLock lock = this.lock;
        //加锁
        lock.lockInterruptibly();
        try {
            //数组为空,在等待不是空通知的瞎弄
            while (count == 0)
                notEmpty.await();
            //取数据
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
 /**
     * Extracts element at current take position, advances, and signals.
     * Call only when holding lock.
     */
    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
                //取数据了
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        //循环
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        //
        if (itrs != null)
            itrs.elementDequeued();
        //通知可以声场数据
        notFull.signal();
        return x;
    }

LinkedBlockingQueue源码

  • 这里底层用的是两把锁ReentrantLock takeLockReentrantLock putLock主以与ArrayBlocingQueue的区别(只有一把锁)
  • 底层是链表,若没有指明初始值则大小为Integer.MaxValue,
  • 因为是两把锁所以锁的粒度比较小,但是可能引起线程不安全的问题,所以需要对队列中的数据有一个准确的把握,因此使用一个原子类AtomicInteger count

底层数据结构

/**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> last;

    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();

put源码


 public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //加锁
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            //LinkedBloking默认的长度Integer.mAXvALUE若没有指定会非常大则不会阻塞
            //若达到设置的阈值,则假数据线程阻塞,也都是while
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            //通过cas进行加1
            c = count.getAndIncrement();
            //
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

 private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
////////////////
////////////////
 /**
     * Signals a waiting take. Called only from put/offer (which do not
     * otherwise ordinarily lock takeLock.)
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

take源码

  public E take() throws InterruptedException {
        E x;
        int c = -1;
        //原子引用(为什么原子运用呢?()
        final AtomicInteger count = this.count;
        //取数据的锁
        final ReentrantLock takeLock = this.takeLock;
        //加锁
        takeLock.lockInterruptibly();
        try {
        //这里获取的数据就是 主内存的数据 (voilitale修饰的数据避免了线程不安全)
            while (count.get() == 0) {
                notEmpty.await();
            }
            //返回链表头部数据
            x = dequeue();
            //线程安全,对链表长度加1
            c = count.getAndDecrement();
			//加入链表长度大于1
            if (c > 1)
                notEmpty.signal();
        } finally {
        //解锁
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

   /**
     * Removes a node from head of queue.
     *
     * @return the node删除链表头部节点并返回
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

小结:ArrayBlockingQueue与LinkedBlocking的区别

  • 底层结构:一个数组一个链表
  • 加锁的方式,ArrayBlocking所有线程一把锁,无论生产线程还是消费线程;BlockingQueue生产者一把锁,消费者一把锁,生产者强putLock,消费者抢takeLock这吧锁
  • LinkedBlockong的长度可以非常长,默认为Integer.Max_vALUE

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