并发编程-06 BlockingQueue及应用场景详解

1、阻塞队列介绍

1.1 Queue接口

Queue队列的接口有:

public interface Queue<E> extends Collection<E> {
    //添加一个元素,成功则返回true,队列满了则抛异常
    boolean add(E e);

    //添加一个元素,成功则返回true,队列满子则返回false
    boolean offer(E e);

    //删除队首元素并返回首元素,队列为空则抛异常
    E remove();

    //删除队首元素并返回首元素,队列为空则返回null
    E poll();
    
    //仅返回队首元素不做删除,队列为空则抛异常
    E element();
	
    //仅返回队首元素不做删除,队列为空则返回null
    E peek();
}

Doug Lea对上述方法在JDK源码注释中做了如下总结:

FunctionThrows exceptionReturns special value
Insert(插入元素)add(e)offer(e)
Remove(移除队首)remove()poll()
Examine(检查队首)element()peek()
1.2 BlockingQueue接口

BlockingQueue接口继承了Queue的特性,兼容原有Queue方法的同时,为了实现Blocking阻塞的能力,又新增了put(E e)和take()方法。

  • 入队逻辑

    • offer(E e):队列没满,插入成功返回true;队列已满,不阻塞,直接返回false
    • offer(E e,long timeout,TimeUnit unit):如果队列满了,设置阻塞等待时间。超时为入队,返回false
    • put(E e) :队列没满时正常插入,队列已满则阻塞,等待队列空出位置。
  • 出队逻辑

    • poll():如果队列有数据,出队;如果没有数据,返回null(不阻塞)
    • poll(long timeout, TimeUnit unit):可设置阻塞时间,如果队列没有数据则阻塞,超过阻塞时间,则返回null
    • take():队列里有数据会正常取出数据并删除;但是如果队列里无数据,则阻塞,直到队列里有数据
方法功能抛出异常返回特定值阻塞阻塞定时长
入队add(e)offer(e)put(e)offer(e,timeout,timeUnit)
出队remove()poll()take()poll(timeout, timeUnit)
获取队首元素element()peek()不支持不支持
1.3 阻塞队列的特性

在这里插入图片描述

1.4 应用场景

BlockingQueue 是线程安全的,我们在很多场景下都可以利用线程安全的队列来优雅地解决我们业务自身的线程安全问题。

生产者/消费者模式是最常见的操作队列的模式,将生产者和消费者解耦,即能做到业务隔离,还能保证线程安全。

1.5 常见阻塞队列
队列描述
ArrayBlockingQueue基于数组结构实现的一个有界阻塞队列
LinkedBlockingQueue基于链表结构实现的一个无界阻塞队列,指定容量为有界阻塞队列
PriorityBlockingQueue支持按优先级排序的无界阻塞队列
DelayQueue基于优先级队列(PriorityBlockingQueue)实现的无界阻塞队列
SynchronousQueue不存储元素的阻塞队列
LinkedTransferQueue基于链表结构实现的一个无界阻塞队列
LinkedBlockingDeque基于链表结构实现的一个双端阻塞队列

2、ArrayBlockingQueue

2.1 ArrayBlockingQueue基础
  • 定容量[有界队列],不可扩容

  • 存储结构final Object[] items; 存储队列内容

  • 一把锁。线程安全由独占锁ReentrantLock保证[入队、出队都由独占锁锁住]

  • 两条指针分别指向消费索引和生产索引

    • int takeIndex; 消费索引,记录出队的位置
    • int putIndex; 生产索引,记录入队的位置
  • 数据结构[环形数组]
    在这里插入图片描述

    • if (++putIndex == items.length) —> putIndex = 0;
    • if (++takeIndex == items.length) —> takeIndex = 0;
2.2 ArrayBlockingQueue适用场景
  • 生产速度和消费速度基本匹配(当生产方或消费方线程很快,则会导致该部分线程一直阻塞)
public class ArrayBlockingQueueDemo {

    public static void main(String[] args) throws Exception {
        //使用ArrayBlockingQueue初始化一个BlockingQueue,指定容量的上限为1024
        BlockingQueue queue = new ArrayBlockingQueue(1024);
        
        Producer producer = new Producer(queue);  //生产者
        Consumer consumer = new Consumer(queue);  //消费者

        new Thread(producer).start();  //开启生产者线程
        new Thread(consumer).start();  //开启消费者线程

        Thread.sleep(4000);
    }
}

public class Consumer implements Runnable{

    protected BlockingQueue queue = null;

    public Consumer(BlockingQueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            System.out.println("consumer "+queue.take());
            System.out.println("consumer "+queue.take());
            System.out.println("consumer "+queue.take());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Producer implements Runnable{

    protected BlockingQueue queue = null;

    public Producer(BlockingQueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            for(int i=0;i<3;i++){
                queue.put(i);
                System.out.println("produce "+i);
                Thread.sleep(10000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
2.3 ArrayBlockingQueue原理

在这里插入图片描述

2.3.2 入队put方法
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}
  • 入队加锁(加可中断锁)

    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    
  • 入队逻辑

    • 直接对指针空位插值,入队完成后,putIndex指针后移
    • ++putIndex == items.length 时,putIndex = 0回到队头
    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();
    }
    
  • 入队阻塞逻辑

    • 当count == items.length 即队列已满,notFull.await();
    • addConditionWaiter() 进入条件队列 阻塞线程
2.3.3 出队take方法
    //加锁 -> 空队等待 -> 取元素
	public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
	//取出某元素
    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;
    }

3、LinkedBlockingQueue

3.1 LinkedBlockingQueue基础
  • 无界队列
    • 队列长度可达Integer.MAX_VALUE
    • 可指定长度
    • 建议初始化定界,避免OOM
  • 存储结构Node
    • E item;存储数据
    • Node next; 单链表结构
  • 两把锁。生产和消费数据分别加锁
    • 读写分离,读写可以并行
    • private final ReentrantLock takeLock = new ReentrantLock();
    • private final Condition notEmpty = takeLock.newCondition();
    • private final ReentrantLock putLock = new ReentrantLock();
    • private final Condition notFull = putLock.newCondition();
  • 两条指针分别指向消费索引和生产索引
    • int takeIndex; 消费索引,记录出队的位置
    • int putIndex; 生产索引,记录入队的位置
  • 数据结构[单链表]
    • 链表长度 private final AtomicInteger count = new AtomicInteger();
    • transient Node head; // 链表头 本身是不存储任何元素的,初始化时item指向null
    • private transient Node last; // 链表尾
3.2 LinkedBlockingQueue使用场景

线程池大部分都是直接使用LinkedBlockingQueue。优势:

  • 可初始化容量,最大Integer.MAX_VALUE
  • 读写锁分离,吞吐量更高
3.3 LinkedBlockingQueue原理

在这里插入图片描述

  • 入队方法

        public void put(E e) throws InterruptedException {
            //入队元素不得为null
            if (e == null) throw new NullPointerException();
            int c = -1;
            //为新入队的元素添加节点
            Node<E> node = new Node<E>(e);
            //独占的put锁
            final ReentrantLock putLock = this.putLock;
            final AtomicInteger count = this.count;
            putLock.lockInterruptibly();
            try {
               //队列满时
                while (count.get() == capacity) {
                    //putLock.newCondition().await() put生产线程阻塞
                    notFull.await();
                }
                //队列不满时入队,直接尾插赋值 last = last.next = node;
                enqueue(node);
                //链表容量 AtomicInteger 自增
                c = count.getAndIncrement();
                //容量为满,将条件等待队列转同步等待队列,准备唤醒阻塞在前面await()方法上的生产者线程
                if (c + 1 < capacity)
                    notFull.signal();
            } finally {
                putLock.unlock();//唤醒阻塞的生产者线程
            }
            if (c == 0)
                //链表长度为0,现在加入元素了,唤醒消费线程
                signalNotEmpty();
        }
    
        private void enqueue(Node<E> node) { 
            // 直接加到last后面,last指向入队元素
            last = last.next = node;
        }    
    
        private void signalNotEmpty() {
            final ReentrantLock takeLock = this.takeLock;
            takeLock.lock();
            try {
                notEmpty.signal();
            } finally {
                takeLock.unlock();
            }
        }
    
  • 出队方法

        public E take() throws InterruptedException {
            E x;
            int c = -1;
            final AtomicInteger count = this.count;
            final ReentrantLock takeLock = this.takeLock;
            takeLock.lockInterruptibly();
            try {
                //链表中无元素,消费者线程需要阻塞
                while (count.get() == 0) {
                    //notEmpty = takeLock.newCondition()
                    notEmpty.await();
                }
                //出队
                x = dequeue();
                //链表节点数量自减
                c = count.getAndDecrement();
                if (c > 1)
                    //节点数>1,表示还有数据需要消费,准备唤醒被阻塞的消费线程
                    notEmpty.signal();//条件队列转的同步等待队列
            } finally {
                takeLock.unlock();//唤醒同步队列中的消费者线程
            }
            if (c == capacity)
                signalNotFull();
            return x;
        }
    
        private E dequeue() {
            //将first置为next节点
            Node<E> h = head;
            Node<E> first = h.next;
            h.next = h; // help GC(丢弃h)
            head = first;
            //first节点的item已经在本次取出,first节点的item不在保留
            E x = first.item;
            first.item = null;
            return x;
        }
    
        private void signalNotFull() {
            final ReentrantLock putLock = this.putLock;
            putLock.lock();
            try {
                notFull.signal();
            } finally {
                putLock.unlock();
            }
        }
    

4、SynchronousQueue

4.1 SynchronousQueue基础

没有缓存容量,生产和消费线程1对1。

数据结构分两种:

  • 栈结构(非公平)
    • 先进后出(LIFO)
    • LifoWaitQueue
  • 队列结构 (公平)
    • 先进先出(FIFO)
    • FifoWaitQueue
  • 锁结构 一把独占锁
    • private ReentrantLock qlock;
  • 容量为0
    • 只是作为多个线程交互的媒介
    • 消费者线程插入,无生产者线程生产数据,此时消费者阻塞[当生产者进入,唤醒阻塞的消费者,交换数据]
    • 生产者线程插入,此时无消费者线程消费数据,此时生产者阻塞[当消费者进入,唤醒阻塞的生产者,交换数据]
4.2 SynchronousQueue使用场景
  • 信息同步传递[消费线程和生产线程 1:1]
  • 非常适合传递性场景做交换工作
  • 线程池 newCachedThreadPool()
    • return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue())
    • 核心线程数为0,生产者来任务后,立即创建同步队列,交由新创建的线程执行
4.3 SynchronousQueue原理

在这里插入图片描述

5、PriorityBlockingQueue

5.1 PriorityBlockingQueue基础
  • 有界性

  • 容量

    • 默认初始化容量 DEFAULT_INITIAL_CAPACITY=11
    • 最大容量 Integer.MAX_VALUE - 8
    • 自定义初始化容量 int initialCapacity
    • 自动扩容 tryGrow(array, cap)
  • 公平性 默认使用优先级最高的出列

  • 数据结构

    • 二叉堆(5.3详述)
      在这里插入图片描述
  • 锁 ReentrantLock lock 存取共用独占锁

    • take()
    • offer()
5.2 PriorityBlockingQueue使用场景
  • 电商抢购,会员的优先级更高
  • 银行办理业务,VIP优先插队
5.3 PriorityBlockingQueue原理

普通线性数组和二叉堆在增删查优先级数据的复杂度对比分析

  • 普通线性数组表示优先级(此处以操作最小值为例)

    • 添加元素

      O(1),末尾添加即可

    • 查找元素

      O(n),遍历数组,找到最小是值元素

    • 删除元素

      • 查找元素 O(n)
      • 删除元素后,后续所有元素前移 O(n)
  • 二叉堆(操作值最小的元素)

    • 添加元素 O(log n)
      在这里插入图片描述

    • 删除元素 O(log n)

      • arr[0],不需要查
      • 删除顶堆数据后,继续保持原堆平衡,子节点与父节点比较大小 ,最大可能发生交换次数 O(log n)
    • 查找元素

      O(1) ,arr[0] 即是最小值

二叉堆结构是优先级队列的精髓内容。

    private static <T> void siftUpComparable(int k, T x, Object[] array) {
        Comparable<? super T> key = (Comparable<? super T>) x;
        while (k > 0) {
            //找到父节点 >>>1 相当于 除以2
            //数组下表可以指示在二叉树的位置 0代表根节点,1代表第二层左,2代表第二层右,以此类推
            int parent = (k - 1) >>> 1;
            //取出父节点的值与当前值作比较,是否需要交换
            Object e = array[parent];
            //新值 > 父节点的值 直接退出循环
            if (key.compareTo((T) e) >= 0)
                break;
            //当前新值比父节点值小,优先级更高
            //新值直接覆盖到根节点
            //父节点的值赋给k
            array[k] = e;
            k = parent;
        //退出当前层高的节点,再与上一层的根节点进行比较
        }
        array[k] = key;
    }

6、DelayQueue

6.1 DelayQueue基础
6.2 DelayQueue使用场景
6.3 DelayQueue原理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旧梦昂志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值