并发编程- 阻塞队列BlockingQueue

数据结构学习网站:

Data Structure Visualization

思维导图:

并发编程- 阻塞队列BlockingQueue_第1张图片

阻塞队列介绍

1.1 队列

1.是限定在一端进行插入,另一端进行删除的特殊线性表。
2.先进先出(FIFO)线性表。
3.允许出队的一端称为队头,允许入队的一端称为队尾。
并发编程- 阻塞队列BlockingQueue_第2张图片
Queue接口
public interface Queue extends Collection {

 //添加一个元素,添加成功返回true, 如果队列满了,就会抛出异常
 boolean add(E e);

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

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

 //返回并删除队首元素,队列为空则返回null
 E poll();

 //返回队首元素,但不移除,队列为空则抛出异常
 E element();

 //获取队首元素,但不移除,队列为空则返回null
 E peek();

 }

1.2 阻塞队列(BlockingQueue

        阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程 安全的队列访问方式: 当阻塞队列插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从 阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。 并发包下很多高级同步类的实 现都是基于BlockingQueue实现的。
BlockingQueue接口
并发编程- 阻塞队列BlockingQueue_第3张图片
并发编程- 阻塞队列BlockingQueue_第4张图片
应用场景:
阻塞队列在实际应用中有很多场景,以下是一些常见的应用场景:
1.线程池
         线程池中的任务队列通常是一个阻塞队列。当任务数超过线程池的容量时,新提交的任务将被放入任务队列中等待执行。线程池中的工作线程从任务队列中取出任务进行处理,如果队列为空,则工作线程会被阻塞,直到队列中有新的任务被提交
2.生产者-消费者模型
         在生产者-消费者模型中,生产者向队列中添加元素,消费者从队列中取出元素进行处理。阻塞队列可以很好地解决生产者和消费者之间的并发问题,避免线程间的竞争和冲突。
并发编程- 阻塞队列BlockingQueue_第5张图片
3.消息队列
         消息队列使用阻塞队列来存储消息,生产者将消息放入队列中,消费者从队列中取出消息进行处理。消息队列可以实现异步通信,提高系统的吞吐量和响应性能,同时还可以将不同的组件解糟,提高系统的可维护性和可扩展性。
4.缓存系统
         缓存系统使用阻塞队列来存储缓存数据,当缓存数据被更新时,它会被放入队列中,其他线程可以从队列中取出最新的数据进行使用。使用阻塞队列可以避免并发更新缓存数据时的竞争和冲突
5.并发任务处理
        在并发任务处理中,可以将待处理的任务放入阻塞队列中,多个工作线程可以从队列中取出任务进行处理。使用阻塞队列可以避免多个线程同时处理同一个任务的问题,并且可以将任务的提交和执行解羯,提高系统的可维护性和可扩展性。

总之,阻塞队列在实际应用中有很多场景,它可以帮助我们解决并发问题,提高程序的性能和可靠性。

1.3 JUC包下的阻塞队列

        BlockingQueue 接口的实现类都被放在了 juc 包中,它们的区别主要体现在存储结构上或对元素操作 上的不同,但是对于take与put操作的原理却是类似的。
并发编程- 阻塞队列BlockingQueue_第6张图片

 2. ArrayBlockingQueue

        ArrayBlockingQueue是最典型的有界阻塞队列,其内部是用数组存储元素的,初始化时需要指定 容量大小,利用 ReentrantLock 实现线程安全。ArrayBlockingQueue可以用于实现数据缓存、限 流、生产者-消费者模式等各种应用。
在生产者-消费者模型中使用时, 如果生产速度和消费速度基本匹配的情况下,使用
ArrayBlockingQueue是个不错选择; 当如果生产速度远远大于消费速度,则会导致队列填满,大量生 产线程被阻塞。

2.1 ArrayBlockingQueue使用

BlockingQueue queue = new ArrayBlockingQueue(1024);
 queue.put("1"); //向队列中添加元素
 Object object = queue.take(); //从队列中取出元素

2.2 ArrayBlockingQueue的原理

并发编程- 阻塞队列BlockingQueue_第7张图片

        ArrayBlockingQueue使用 独占锁ReentrantLock实现线程安全 ,入队和出队操作使用同一个锁对象, 也就是只能有一个线程可以进行入队或者出队操作;这也就意味着生产者和消费者无法并行操作,在 高并发场景下会成为性能瓶颈。
数据结构
利用了Lock锁的Condition通知机制进行阻塞控制。
核心:一把锁,两个条件
//数据元素数组
 final Object[] items;
 //下一个待取出元素索引
 int takeIndex;
 //下一个待添加元素索引
 int putIndex;
 //元素个数
 int count;
 //内部锁
 final ReentrantLock lock;
 //消费者
 private final Condition notEmpty;
 //生产者
 private final Condition notFull; 

 public ArrayBlockingQueue(int capacity) {
 this(capacity, false);
 }
 public ArrayBlockingQueue(int capacity, boolean fair) {
 ...
 lock = new ReentrantLock(fair); //公平,非公平
 notEmpty = lock.newCondition();
 notFull = lock.newCondition();
 }
入队put方法
public void put(E e) throws InterruptedException {
        //检查是否为空
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //加锁,如果线程中断抛出异常
        lock.lockInterruptibly();
        try {
            //阻塞队列已满,则将生产者挂起,等待消费者唤醒
            //设计注意点: 用while不用if是为了防止虚假唤醒
            while (count == items.length)
                notFull.await(); //队列满了,使用notFull等待(生产者阻塞)
            // 入队
            enqueue(e);
        } finally {
            lock.unlock(); // 唤醒消费者线程
        }
    }

    private void enqueue(E x) {
        final Object[] items = this.items;
        //入队 使用的putIndex
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0; //设计的精髓: 环形数组,putIndex指针到数组尽头了,返回头部
        count++;
        //notEmpty条件队列转同步队列,准备唤醒消费者线程,因为入队了一个元素,肯定不为空了
        notEmpty.signal();
    }
思考: 为什么ArrayBlockingQueue对数组操作要设计成双指针?
并发编程- 阻塞队列BlockingQueue_第8张图片
        使用双指针的好处在于可以避免数组的复制操作。如果使用单指针,每次删除元素时需要将后面的元素全部向前移动,这样会导致时间复杂度为 O(n)。而使用双指针,我们可以直接将 takeIndex 指向下 一个元素,而不需要将其前面的元素全部向前移动。同样地,插入新的元素时,我们可以直接将新元 素插入到 putIndex 所指向的位置,而不需要将其后面的元素全部向后移动。 这样可以使得插入和删除 的时间复杂度都是 O(1) 级别,提高了队列的性能。
出队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() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex]; //取出takeIndex位置的元素
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0; //设计的精髓: 环形数组,takeIndex 指针到数组尽头了,返回头部
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        //notFull条件队列转同步队列,准备唤醒生产者线程,此时队列有空位
        notFull.signal();
        return x;
    }

3. LinkedBlockingQueue

        LinkedBlockingQueue是一个基于链表实现的阻塞队列 ,默认情况下,该阻塞队列的大小为
Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队列 ,代表 它几乎没有界限, 队列可以随着元素的添加而动态增长, 但是 如果没有剩余内存,则队列将抛出OOM错 误。 所以 为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一 个队列的大小。

3.1 LinkedBlockingQueue使用

//指定队列的大小创建有界队列
 BlockingQueue boundedQueue = new LinkedBlockingQueue<>(100);
 //无界队列
 BlockingQueue unboundedQueue = new LinkedBlockingQueue<>();

3.2 LinkedBlockingQueue原理

        LinkedBlockingQueue内部由 单链表 实现, 只能从head取元素,从tail添加元素
        LinkedBlockingQueue采用 两把锁 的锁分离技术 实现入队出队互不阻塞 ,添加元素和获取元素都有独 立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。
并发编程- 阻塞队列BlockingQueue_第9张图片
数据结构
         // 容量,指定容量就是有界队列
        private final int capacity;
         // 元素数量
         private final AtomicInteger count = new AtomicInteger();
         // 链表头 本身是不存储任何元素的,初始化时item指向null
        transient Node head;
         // 链表尾
         private transient Node last;
         // take锁 锁分离,提高效率
         private final ReentrantLock takeLock = new ReentrantLock();
         // notEmpty条件
        // 当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
        private final Condition notEmpty = takeLock.newCondition();
        // put锁
        private final ReentrantLock putLock = new ReentrantLock();
        // notFull条件
        // 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
        private final Condition notFull = putLock.newCondition();

         //典型的单链表结构
        static class Node {
         E item; //存储元素
         Node next; //后继节点 单链表结构
         Node(E x) { item = x; }
         }

构造器:
public LinkedBlockingQueue(){
        // 如果没传容量,就使用最大int值初始化其容量
        this(Integer.MAX VALUE);
    }



    public LinkedBlockingQueue(int capacity) {
        if (capacity <=) throw new IllegalArgumentException();
        this.capacity = capacity;// 初始化head和last指针为空值节点
         last = head = new Node(nul1);
    }
 入队put方法
public void put(E e) throws InterruptedException {
        // 不允许null元素
        if (e == null) throw new NullPointerException();
        int c = -1;
        // 新建一个节点
        Node node = new Node(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        // 使用put锁加锁
        putLock.lockInterruptibly();
        try {
            // 如果队列满了,就阻塞在notFull上等待被其它线程唤醒(阻塞生产者线程)
            while (count.get() == capacity) {
                notFull.await();
            }
            // 队列不满,就入队
            enqueue(node);
            c = count.getAndIncrement();// 队列长度加1,返回原值
            // 如果现队列长度小于容量,notFull条件队列转同步队列,准备唤醒一个阻塞在notFull条件上
            //的线程(可以继续入队)
            // 这里为啥要唤醒一下呢?
            // 因为可能有很多线程阻塞在notFull这个条件上,而取元素时只有取之前队列是满的才会唤醒
            notFull, 此处不用等到取元素时才唤醒
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock(); // 真正唤醒生产者线程
        }
        // 如果原队列长度为0,现在加了一个元素后立即唤醒阻塞在notEmpty上的线程
        if (c == 0)
            signalNotEmpty();
    }

    private void enqueue(Node node) {
        // 直接加到last后面,last指向入队元素
        last = last.next = node;
    }

    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();// 加take锁
        try {
            notEmpty.signal();// notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程
        } 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加锁
        takeLock.lockInterruptibly();
        try {
            // 如果队列无元素,则阻塞在notEmpty条件上(消费者线程阻塞)
            while (count.get() == 0) {
                notEmpty.await();
            }
            // 否则,出队
            x = dequeue();
            c = count.getAndDecrement();//长度-1,返回原值
            if (c > 1)// 如果取之前队列长度大于1,notEmpty条件队列转同步队列,准备唤醒阻塞在
                //notEmpty上的线程,原因与入队同理
                notEmpty.signal();
        } finally {
            takeLock.unlock(); // 真正唤醒消费者线程
        }
        // 为什么队列是满的才唤醒阻塞在notFull上的线程呢?
        // 因为唤醒是需要加putLock的,这是为了减少锁的次数,所以,这里索性在放完元素就检测一下,未
        //满就唤醒其它notFull上的线程,
        // 这也是锁分离带来的代价
        // 如果取之前队列长度等于容量(已满),则唤醒阻塞在notFull的线程
        if (c == capacity)
            signalNotFull();
        return x;
    }

    private E dequeue() {
        // head节点本身是不存储任何元素的
        // 这里把head删除,并把head下一个节点作为新的值
        // 并把其值置空,返回原来的值
        Node h = head;
        Node first = h.next;
        h.next = h; // 方便GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();// notFull条件队列转同步队列,准备唤醒阻塞在notFull上的线程
        } finally {
            putLock.unlock(); // 解锁,这才会真正的唤醒生产者线程
        }
    }
3.3 LinkedBlockingQueue与ArrayBlockingQueue对比
        LinkedBlockingQueue是一个阻塞队列,内部由两个ReentrantLock来实现出入队列的线程安
全,由各自的Condition对象的await和signal来实现等待和唤醒功能。它和ArrayBlockingQueue的
不同点在于:
         队列大小有所不同 ArrayBlockingQueue 有界的初始化必须指定大小 ,而 LinkedBlockingQueue 可以是有界的也可以是无界的 (Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能 会造成内存溢出等问题。
        数据存储容器不同 ,ArrayBlockingQueue采用的是 数组作为数据存储容器 ,而LinkedBlockingQueue采用的则是 以Node节点作为连接对象的链表
        由于 ArrayBlockingQueue 采用的是数组的存储容器 ,因此在 插入 删除 元素时 不会产生销毁任何额外的对象实例 ,而 LinkedBlockingQueue 会生成一个额外的Node对象 。这可能在长时间内需要高效并发地处理大批量 数据的时,对于GC可能存在较大影响。
        两者的实现队列添加或移除的锁不一样, ArrayBlockingQueue实现的队列中的锁是没有分离的 ,即添加操作和 移除操作采用的同一个ReenterLock锁,而 LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是 putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量, 也意味着在高并发的情况下生产者和消费 者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

你可能感兴趣的:(java,数据库,开发语言)