万字总结之阻塞队列以及常见面试题目

阻塞队列(BlockingQueue)

基本概念

​ 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加操作支持阻塞的插入和移除方法。

​ 即在多线程中,阻塞的意思就是,在某些情况下会挂起线程,一旦条件成熟,被阻塞的线程将会被自动唤醒。

  • 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程直到队列不满
  • 支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空
方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,util)
检查方法 element() peek() 不可用 不可用

阻塞队列一共有7种:

队列名 作用
ArrayBlockingQueue 由数组结构组成的有界阻塞队列
LinkedBlockingQueue 由链表结构组成的有界阻塞队列
PriorityBlockingQueue 支持优先级排序的无界阻塞队列
DelayQueue 使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue 不存储元素的阻塞队列,也就单个元素的队列
LinkTransferQueue 由链表结构组成的无界阻塞队列
LinkBlockingQueue 由链表结构组成的双向阻塞队列

阻塞的实现

​ ReentrantLock(锁)+Condition(条件状态)实现队列的阻塞,通过等待/通知机制。来实现线程之间的通信。

类似于Object的wait()和notify()。通过Synchronized,在锁中使用wait()和notify()达到线程之间的通信

ArrayBlockingQueue

基于数组实现有界的阻塞队列(循环数组)

类的继承

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable

主要成员变量

	private static final long serialVersionUID = -817911632652898426L;

   final Object[] items; //底层存储元素的数组

   int takeIndex; //进行取操作时的下标

   int putIndex;//进行放操作时的下标
   
   int count;//队列中元素的数量
   
   final ReentrantLock lock;//阻塞时用的锁

   private final Condition notEmpty;//满时的condition队列

   private final Condition notFull;//空时的condition队列


构造器

参数由容量和全局锁是否是公平锁(默认是非公平锁)

    public ArrayBlockingQueue(int capacity, boolean fair) {
    
    
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

除此之外还有一个接受三个参数的构造器,多出一个集合类型,即将整个集合挪入

 public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
    
    
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
    
    
            int i = 0;
            try {
    
    
                for (E e : c) {
    
    
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
    
    
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
    
    
            lock.unlock();
        }
    }

​ 其中第五行代码中获取互斥锁,解释为锁的母的不是为了互斥,而是为了保证可见性。

扫描二维码关注公众号,回复: 11778394 查看本文章

​ 由于ArrayBlockingQueue操作的其实是一个items数组,这个数组不具备线程安全,所以保证可见性解释保证items的可见性。

主要方法

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();
        }
    }
  • 首先判断添加的是否为空,为空就抛出异常
  • 然后给put方法上锁
  • 当集合元素数量和集合长度相等时,调用put方法的线程将会被放入notFull队列上等待
  • 如果不相等,意味着还是有位置的则之间enquue(),让e进入队列
enqueue()
    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操作基本就是put反过来,真正实现的是dequeue

LinkedBlockingQueue

LinkedBlockingQueue底层是基于链表实现的,所以其基本成员变量和LinkedList差不多

类的继承

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable

构造器

手动设置容量,无参构造器是默认容量为最大容量

 public LinkedBlockingQueue(int capacity) {
    
    
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

也支持将整个集合挪入队列中,默认容量同样是最大容量

 public LinkedBlockingQueue(Collection<? extends E> c) {
    
    
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
    
    
            int n = 0;
            for (E e : c) {
    
    
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
    
    
            putLock.unlock();
        }
    }

主要成员变量

​ 链表就一定会有家店,内部节点类和ArrayBlockingQueue不同的是,它有两个全局锁,一个负责放元素,一个负责取元素

 static class Node<E> {
    
    
        E item;

        Node<E> next;

        Node(E x) {
    
     item = x; }
    }

​ 除了节点以外

private transient Node<E> last;//尾节点

transient Node<E> head;//头节点

private final AtomicInteger count = new AtomicInteger();//计算当前阻塞队列中的元素个数 

private final int capacity;//容量
 
 //获取并移除元素时使用的锁,如take, poll, etc
private final ReentrantLock takeLock = new ReentrantLock();

//notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程
private final Condition notEmpty = takeLock.newCondition();

//添加元素时使用的锁如 put, offer, etc
private final ReentrantLock putLock = new ReentrantLock();

//notFull条件对象,当队列数据已满时用于挂起执行添加的线程
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.
             */
            while (count.get() == capacity) {
    
    
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
    
    
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

PriorityBlockQueue

​ 其是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排列(但不能保证同优先级元素的顺序)

​ 继承Compareable类实现compareTo()方法来指定元素排序规则,或者初始化

DelayQueue

​ 基于PriorityQueue的延时阻塞队列,DelayQueue中的元素只有当其延时时间到达,才能够去当前队列中获取到该元素,DelayQueue是一个无界队列。主要用于缓存系统的设计(保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦获取元素,表示缓存有效期到了)、定时任务系统的设计(使用其保存要执行的任务和执行时间,一旦获取到就开始执行)

​ 实现Delayed的三个步骤

  • 第一步:继承Delayed接口
  • 第二步:实现getDelay(TimeUnit unit),该方法返回当前元素还需要延时多长时间,单位是纳秒
  • 第三步:实现compareTo方法来指定元素的顺序。

SynchronousQueue

​ 其是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。如果设置为true,则等待的线程会采用先进先出的顺序访问队列

​ SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue.。

LinkTransferQueue

​ 其是一个由链表结构组成的无界阻塞队列。相对于其他阻塞队列,其多了tryThransfer和trnsfer方法

transfer方法

​ 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回

tryTransfer方法

​ tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。

​ 对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true

LinkedBlockingDeque

​ 其是一个由链表结构组成的双向阻塞队列。即可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。

​ 相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,

​ 另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,

​ 在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。

应用:消费者生产者模式

public class Data {
    
    
    //flag 表示是否生产,默认生产
    private volatile boolean flag=true;
    //nums 表示产品
    private AtomicInteger nums=new AtomicInteger();
    //使用阻塞队列实现
    BlockingDeque<Object>queue=null;
    public Data(BlockingDeque<Object> queue){
    
    
        this.queue=queue;
    }
    // 生产者
    public void produce()throws Exception{
    
    
        String data =null;
        boolean retValue;
        while (flag){
    
    
            data =nums.incrementAndGet()+"";
            retValue =queue.offer(data,2L, TimeUnit.SECONDS);
            System.out.println(Thread.currentThread().getName()+"插入结果是:"+retValue);
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName()+"稍安勿躁,一会就好");
    }
    // 消费者
    public void consumer() throws Exception{
    
    
        Object result=null;
        while (flag){
    
    
            result=queue.poll(2L,TimeUnit.SECONDS);
            if(result==null||((String)result).equalsIgnoreCase("")){
    
    
                flag=false;
            }
            System.out.println(Thread.currentThread().getName()+"消费资源成功");
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

总结

  • ArrayBlockingQueue和LinkedBlockingQueue的区别和联系?
    • 数据存储容器不一样,ArrayBlockingQueue采用数组去存储数据、LinkedBlockingQueue采用链表去存储数据,在添加和删除队列中的元素的时候,会创建和销毁对象,在高并发和大量数据的时候,GC压力太大。
    • ArrayBlockingQueue是有界的,初始化时必须要指定容量;LinkedBlockingQueue默认是无界的,Integer.MAX_VALUE, 当添加速度大于删除速度、有可能造成内存溢出。
    • ArrayBlockingQueue在读和写使用的锁是一样的,即添加操作和删除操作使用的是同一个ReentrantLock,没有实现锁分离;LinkedBlockingQueue实现了锁分离,添加的时候采用putLock、删除的时候采用takeLock,这样能提高队列的吞吐量。
  • ArrayBlockingQueue可以使用两把锁提高效率吗?
    • 不能,主要原因是ArrayBlockingQueue底层循环数组来存储数据,LinkedBlockingQueue底层 链表来存储数据,链表队列的添加和删除,只是和某一个节点有关,为了防止head和last相互影响,就需要有一个原子性的计数器,每个添加操作先加入队列,计数器+1,这样是为了保证队列在移除的时候, 长度是大于等于计数器的,通过原子性的计数器,使得当前添加和移除互不干扰。对于循环数据来说,当我们走到最后一个位置需要返回到第一个位置,这样的操作是无法原子化,只能使用同一把锁来解决。

最后

  • 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持
  • 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我
  • 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。

猜你喜欢

转载自blog.csdn.net/issunmingzhi/article/details/107860782