Java-并发篇-09-关于阻塞队列

1. 简述

阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如图所示:
在这里插入图片描述
线程1往阻塞队列中添加元素,线程2从队列中移除元素

  • 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞.
  • 当阻塞队列是满时,往队列中添加元素的操作将会被阻塞.

同样试图往已满的阻塞队列中添加新圆度的线程同样也会被阻塞,知道其他线程从队列中移除一个或者多个元素或者全清空队列后使队列重新变得空闲起来并后续新增.

2. 为什么使用阻塞队列?

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即线程阻塞),一旦条件满足,被挂起的线程优惠被自动唤醒

为什么需要使用BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因BlockingQueue都一手给你包办好了,在concurrent包 发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度.

3. BlockingQueue核心方法

接口有四组API,一般选用第四组

方法类型抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e,time,unit)
移除remove()poll()take()poll(time,unit)
检查element()peek()不可用不可用
类型含义
抛出异常当阻塞队列满时,再往队列里面add插入元素会抛legalStateException: Queue full 当阻塞队列空时,再往队列Remove元素时候回抛出NoSuchElementException
特殊值插入方法,成功返回true失败返回false 移除方法,成功返回元素,队列里面没就返回null
一直阻塞当阻塞队列满时,生产者继续往队列里面put元素,队列会一直阻塞直到put数据or响应中断退出 当阻塞队列空时,消费者试图从队列take元素,队列会一直阻塞消费者线程直到队列可用.
超时退出当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程就会退出

4. 架构梳理

在这里插入图片描述

5. 子类分析

5.1 有界队列

  • ArrayBlockingQueue:
    由数组结构组成的有界阻塞队列.
  • LinkedBlockingQueue:
    由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列.
  • SynchronousQueue:
    不存储元素的阻塞队列,也即是单个元素的队列.

5.2 无界队列

  • PriorityBlockingQueue:
    支持优先级排序的无界阻塞队列.
  • DelayQueue:
    使用优先级队列实现的延迟无界阻塞队列.
  • LinkedTransferQueue:
    由链表结构组成的无界阻塞队列.
  • ConcurrentLinkedQueue:
    由CAS操作组成的无锁队列.

6. 关于同步队列SynchronousQueue Demo

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/8/2 15:46
 * @description: 同步队列,生产一个消费一个
 */
public class SynchronousQueueDemo {

    public static void main(String[] args) {

        BlockingQueue<String> b = new SynchronousQueue<>();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t\t put 1");
            try {
                b.put("1");
                System.out.println(Thread.currentThread().getName() + "\t\t put 2");
                b.put("2");
                System.out.println(Thread.currentThread().getName() + "\t\t put 3");
                b.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();


        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t\t take 1 ");
            try {
                TimeUnit.SECONDS.sleep(3);
                b.take();
                System.out.println(Thread.currentThread().getName() + "\t\t take 2 ");
                TimeUnit.SECONDS.sleep(3);
                b.take();
                System.out.println(Thread.currentThread().getName() + "\t\t take 3 ");
                TimeUnit.SECONDS.sleep(3);
                b.take();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }, "BBB").start();


    }
}

7. 案列

7.1 传统版消费模式

package 阻塞队列.ProdConsumer_TraditionDemo;

import jdk.nashorn.internal.ir.IfNode;

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

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/8/2 20:43
 * @description: 传统的生产者消费者  Sync  wait notify
 * <p>
 * 题目: 一个初始值为零的变量,两个线程对其交替操作,一个加1,一个减1,来5轮
 */
class ShareDataDemo {

    private int number = 0;
    private Lock lock = new ReentrantLock();
    //新版的,旧版的是object自带的wait和notify
    private Condition condition = lock.newCondition();


    /**
     * 线程操作资源类,+1;
     */
    public void increment() {

        lock.lock();
        try {
            while (number != 0) {
                //等待, 不需要生产
                condition.await();
            }
            //干活
            number++;
//            System.out.println(Thread.currentThread().getName() + "\t" + number);
            //通知唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }


    }


    /**
     * 线程操作资源类-1
     */
    public void deCreament() {

        lock.lock();
        try {
            while (number == 0) {
                condition.await();
            }
            number--;
//            System.out.println(Thread.currentThread().getName() + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }


}


public class ProdConsumerTradition {

    public static void main(String[] args) {

        ShareDataDemo s = new ShareDataDemo();

        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                s.increment();
                System.out.println(Thread.currentThread().getName() + "生产");
            }, "A").start();
        }

        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                s.deCreament();
                System.out.println(Thread.currentThread().getName() + "消费");
            }, "B").start();
        }
    }
}

7.2 阻塞队列版消费模式

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/8/4 23:13
 * @description: volatile/CAS/atomic/BlockQueue/线程交互/原子引用
 */

class MyResour {
    //高并发下的程序,资源可见性
    private volatile boolean Falg = true;  //默认开启,进行生产+消费
    private AtomicInteger atomicInteger = new AtomicInteger();

    BlockingQueue<String> blockingQueue = null;

    //写适配接口,不要写具体类,
    public MyResour(BlockingQueue blockingQueue) {
        this.blockingQueue = blockingQueue;
        System.out.println(blockingQueue.getClass().getName());
    }

    public void Prod() throws InterruptedException {

        String data = null;
        boolean retVaule;

        //不要使用if,防止虚假通知
        while (Falg) {
            data = atomicInteger.incrementAndGet() + "";
            retVaule = blockingQueue.offer(data, 2, TimeUnit.SECONDS);


            if (retVaule) {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列" + data + "成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列" + data + "失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }

        System.out.println(Thread.currentThread().getName() + "\t大老板叫停,停止生产" + "Falg:" + Falg);
    }


    public void Consumer() throws InterruptedException {

        String result = null;

        while (Falg) {
            result = blockingQueue.poll(2, TimeUnit.SECONDS);

            if (null == result || result.equalsIgnoreCase("")) {
                Falg = false;
                System.out.println(Thread.currentThread().getName() + "2秒没收到蛋糕,消费退出");
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t消费队列" + result + "成功");
        }
    }

    public void stop() {
        this.Falg = false;
        System.out.println("大老板叫停");
    }

}


public class ProdConsumer_BlockQueueDemo {

    public static void main(String[] args) {

        MyResour myResour = new MyResour(new ArrayBlockingQueue(10));


        new Thread(() -> {
            try {
                myResour.Prod();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Prod").start();


        new Thread(() -> {
            try {
                myResour.Consumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Consumer").start();


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

       myResour.stop();
    }

}


8. 阻塞队列是线程安全的嘛?

8.1 简介

阻塞队列,即BlockingQueue,它是一个接口,继承自Queue 接口,是队列的一种。Queue 和 BlockingQueue 都是在 Java 5 中加入的。

public interface BlockingQueue<E> extends Queue<E>{...}

阻塞队列是线程安全的,典型的应用场景是在生产者/消费者模式中,用于存储数据,保证再多线程下的正确运行
在这里插入图片描述
除了BlockingQueue,Queue接口的实现类和子类还有很多,如下图所示:
在这里插入图片描述
上述实现类和子类中,除了Deque都是线程安全的,而这些线程安全的队列可以分为阻塞队列非阻塞队列两大类。

  • 阻塞队列就是BlockingQueue 接口的实现类,主要有6种:
    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • SynchronousQueue
    • DelayQueue
    • PriorityBlockingQueue
    • LinkedTransferQueue
  • 非阻塞队列就是ConcurrentLinkedQueue
    这个类不会让线程阻塞,利用 CAS 保证了线程安全。

Deque 是一个双端队列,从头和尾都能添加和删除元素;而普通的 Queue 只能从一端进入,另一端出去

8.2 BlockingQueue的常见方法

BlockingQueue中和添加、删除相关的方法有8个,它们的区别仅在于特殊情况:当队列满了无法添加元素,或者是队列空了无法移除元素时,不同组的方法对于这种特殊情况会有不同的处理方式:

  • 抛出异常:add、remove、element
  • 返回结果但不抛出异常:offer、poll、peek
  • 阻塞:put、take

8.2.1 add、remove、element方法

这组方法在处理特殊情况时,会抛出异常
add方法用于添加元素,如果队列满了,就会抛出异常来提示队列已满

private static void addTest() {
    BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    blockingQueue.add(1);
    blockingQueue.add(1);
    blockingQueue.add(1);
}

// 运行结果
Exception in thread "main" java.lang.IllegalStateException:Queue full

remove 方法用于删除元素,如果队列为空,抛出异常;

private static void removeTest() {
    ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    blockingQueue.add(1);
    blockingQueue.add(1);
    blockingQueue.remove();
    blockingQueue.remove();
    blockingQueue.remove();
}

// 运行结果
Exception in thread "main" java.util.NoSuchElementException

element 方法用于返回队列头结点,但并不删除。和remove 方法一样,如果队列为空,抛出异常

private static void elementTest() {
    ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    blockingQueue.element();
}

// 运行结果
Exception in thread "main" java.util.NoSuchElementException

8.2.2 offer、poll、peek方法

这组方法在处理特殊情况时,会返回一个提示,而不会抛出异常。

offer 方法用来插入一个元素,并用返回值来提示插入是否成功。如果添加成功会返回 true,而如果队列已经满了,返回false

private static void offerTest() {
    ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    System.out.println(blockingQueue.offer(1));
    System.out.println(blockingQueue.offer(1));
    System.out.println(blockingQueue.offer(1));
}

// 运行结果
true
true
false

poll 方法用于移除并返回队列的头节点,如果当队列里面是空的,没有任何东西可以移除的时候,便会返回 null。正因为如此,不允许往队列中插入 null 值,否则没有办法区分返回的 null 是一个提示还是一个真正的元素

private static void pollTest() {
    ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(3);
    blockingQueue.offer(1);
    blockingQueue.offer(2);
    blockingQueue.offer(3);
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
}

// 运行结果
1
2
3
null

peek 方法用于返回队列的头元素但并不删除。如果队列里面是空的,会返回 null 作为提示。

private static void peekTest() {
    ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(2);
    System.out.println(blockingQueue.peek());
}

// 运行结果
null

另外,offer 和 poll 都有带超时时间的重载方法。

offer(E e, long timeout, TimeUnit unit)

以offer为例,它有三个参数,分别是元素、超时时长和时间单位。插入成功会返回 true;如果队列满了导致插入不成功,则会等待指定的超时时间,如果时间到了依然没有插入成功,就会返回 false。

8.2.3 put、take方法

这一组方法在处理特殊情况时,会采用阻塞等待的策略,这也是阻塞队列名字的由来

  • put 方法用于插入元素。如果队列已满,既不会立刻返回 false 也不会抛出异常,而是让插入的线程陷入阻塞状态,直到队列里有了空闲空间,此时队列就会让之前的线程解除阻塞状态,并把刚才那个元素添加进去。
  • take 方法用于获取并移除队列的头结点,当队列为空,则阻塞线程,直到队列里有数据;一旦队列里有数据了,就会立刻解除阻塞状态,并且取到数据。

8.3 常见的阻塞队列

8.3.1 ArrayBlockingQueue

一种有界队列,底层基于数组实现,利用 ReentrantLock 实现线程安全

构造函数中可以指定队列容量,一旦指定后续不可以扩容。同时可以指定是否公平。

ArrayBlockingQueue(int capacity, boolean fair)

8.3.2 LinkedBlockingQueue

一种近似无界的队列(实际最大容量是整型的最大值 Integer.MAX_VALUE),内部基于链表实现

8.3.3 SynchronousQueue

这种队列的容量为0,不能存储元素,每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。它的作用就是直接传递

由于SynchronousQueue的特性,它的一些方法返回值很独特:

// peek方法直接返回null
public E peek() {
    return null;
}

// size方法直接返回0
public int size() {
    return 0;
}

// isEmpty方法直接返回true
public boolean isEmpty() {
    return true;
}

8.3.4 PriorityBlockingQueue

这种队列可以自定义内部元素的排列顺序,也是一个(可以看做)无界的阻塞队列。通过实现compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。同时,插入队列的对象必须是可比较大小的,也就是 Comparable 的,否则会抛出 ClassCastException 异常。

PriorityBlockingQueue的take方法会阻塞,但是由于无界,put方法永远不会阻塞。

8.3.5 DelayQueue

这种队列具有“延迟”的功能,可以指定任务延迟多久之后执行。

同时,它也是一个无界队列,放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了 Comparable 接口,因此可以比较和排序。

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

实现Delayed接口需要实现getDelay方法,该方法返回的是“还剩下多长的延迟时间才会被执行”。

DelayQueue中的元素会根据延迟时间的长短被放到队列的不同位置,越靠近队列头代表越早过期。其内部复用了PriorityBlockingQueue的逻辑进行排序。

8.4 阻塞队列和非阻塞队列的线程安全原理

无论是阻塞队列还是非阻塞队列,都是可以保证线程安全的。

8.4.1 阻塞队列的线程安全

以ArrayBlockingQueue 的源码为例,以下是该类的重要属性:

final Object[] items;
int takeIndex;
int putIndex;
int count;

// 与线程安全有关
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
  • Object 类型的数组用于存储元素;
  • takeIndex 和 putIndex用来标明下一次读取和写入位置的;
  • count 用来计数,它所记录的就是队列中的元素个数。而剩下的三个属性,一个是 ReentrantLock,另外两个Condition 分别是由 ReentrantLock 产生,这三个属性是实现线程安全最核心的工具

以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();
    }
}

该方法内部逻辑是:

  • 首先用checkNotNull 方法去检查插入的元素是不是 null
  • 不为null时,用 ReentrantLock 上锁,并且上锁方法是 lock.lockInterruptibly()。这意味着在尝试获取锁但还没拿到锁的期间可以响应中断
  • 接着是try finally 代码块,finally 中会去解锁,try中的while 循环会会检查当前队列是不是已经满了。如果队列已满,便会进行等待,直到有空余的时候跳出循环,调用 enqueue 方法让元素进入队列,最后用 unlock 方法解锁。

这就是ArrayBlockingQueue 中put方法的线程安全策略。其实在线程基础中用Condition 实现生产者消费者模式,本质上就是简易版的BlockingQueue。

类似的,LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue等也是利用了 ReentrantLock 来保证线程安全,只不过细节有差异,比如 LinkedBlockingQueue 的内部有两把锁,分别锁住队列的头和尾,比共用同一把锁的效率更高,不过总体思想都是类似的。

8.4.2 非阻塞队列的线程安全

以ConcurrentLinkedQueue为例,查看offer方法的源码:

public boolean offer(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        if (q == null) {
            // p is last node
            if (p.casNext(null, newNode)) {
                // Successful CAS is the linearization point
                // for e to become an element of this queue,
                // and for newNode to become "live".
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q)
            // We have fallen off list.  If tail is unchanged, it
            // will also be off-list, in which case we need to
            // jump to head, from which all live nodes are always
            // reachable.  Else the new tail is a better bet.
            p = (t != (t = tail)) ? t : head;
        else
            // Check for tail updates after two hops.
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

该方法整体是一个大的for循环,而且是明显的死循环。
代码中的 p.casNext 方法,正是利用了 CAS 来操作的,而且这个死循环去配合 CAS 也就是典型的乐观锁的思想。

boolean casNext(Node<E> cmp, Node<E> val) {
    return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

8.5 阻塞队列的选择

阻塞队列很重要的一个应用场景就是线程池的,常见的线程池有5种,每种线程池的阻塞队列选择不同,具体的情况在线程池中已经进行了较为细致的阐述。

在其他的应用场景中,选择合适的阻塞队列可以从以下几点考虑:

  • 功能:如,是否需要阻塞队列排序,如优先级排序、延迟执行等
  • 容量:是否有存储的要求,还是只需要“直接传递”
  • 能否扩容:是否需要队列能够动态扩容
  • 内存结构:不同阻塞队列的底层时间不同,如果对性能有要求可以从内存的结构角度去考虑
  • 性能:比如 LinkedBlockingQueue 由于拥有两把锁,并发性能更好;SynchronousQueue只需要“直接传递”,而不需要存储,性能更好

9. LinkedList/Deque中add/offer/push,remove/pop/poll的区别

最近在使用LinkedList/Deque的时候,发现其中有很多类似的方法,经过一番学习和测试以后,得出以下结论:这些方法从设计之初,分别来自于集合Collections,队列Queue,栈Stack,双端队列Deque,因此它们是有语义的,不建议笼统归为添加/删除。

  • add和remove是一对,源自Collection;
    在这里插入图片描述

  • offer和poll是一对,源自Queue;
    在这里插入图片描述

  • push和pop是一对,源自Deque
    其本质是栈(Stack类由于某些历史原因,官方已不建议使用,使用Deque代替);
    在这里插入图片描述

  • addFirst,addLast,offerFirst,offerLast,pollFirst,pollLast,peekFirst,peekLast 源自Deque,其本质是双端队列
    在这里插入图片描述

由于历史原因,在Java中,官方不建议使用Stack类,而是使用Deque代替,也就是说,接口Deque是栈和双端队列这两种数据结构的集合体。

  • add/remove源自集合,所以添加到队尾,从队头删除;
  • offer/poll源自队列(先进先出 => 尾进头出),所以添加到队尾,从队头删除;
  • push/pop源自栈(先进后出 => 头进头出),所以添加到队头,从队头删除;
  • offerFirst/offerLast/pollFirst/pollLast源自双端队列(两端都可以进也都可以出),根据字面意思,offerFirst添加到队头,offerLast添加到队尾,pollFirst从队头删除,pollLast从队尾删除。

9.1 add系列

在这里插入图片描述
add()和addLast()的区别:
在这里插入图片描述

可以看出都是往最后增加元素,区别就是add()多返回了一个boolean。
另外add()还可以同时传入两个元素,分别表示下标和要增加的元素。

addFirst()和addLast()区别:

addFirst()和addLast()底层都是节点,只不过addFirst()是和首节点连接,addLast()是和尾节点连接。

9.2 offer()系列

offer其实和add操作类似
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.3 peek()系列

peek()和peekFirst()都是读取首个元素,peekLast()读取末尾元素。都只是读取,不会改变集合。
在这里插入图片描述

9.4 pop系列

在这里插入图片描述
poll()和pollFirst()没有区别,弹出首个元素。
pollLast()弹出尾元素。

pop()和poll()的区别:
当头节点为null时,pop()抛出异常,poll()返回null

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Alan0517

感谢您的鼓励与支持!

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

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

打赏作者

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

抵扣说明:

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

余额充值