【高并发系列-2】java线程池的阻塞队列详解

引言:之前上一篇 java线程池复用原理 详细解释了线程池复用原理,其中一个关键就是阻塞队列,这一篇将会详细介绍阻塞队列的特征。

阻塞队列是什么?和普通队列的区别是什么

阻塞队列对应的类名叫java.util.concurrent.BlockingQueue,它的父类是java.util.Queue,都是接口,而Queue还继承了Collection接口,所以具备size() isEmpty() contains() iterator()等方法

Queue

对于Queue我们比较熟悉,队列是一个先进先出的数据结构,可以往队尾加入元素,从队头获取或弹出一个元素,常见的方法有:
1、boolean add(E element) 往队尾插入元素,如果超出capacity,则抛出异常
2、boolean offer(E element) 往队尾插入元素,成功返回true,失败(capacity restrictions)返回false
3、E pool() 从对头弹出一个元素,如果队列为空,则返回null。
4、E peek() 获取队头元素,但不弹出,如果队列为空,则返回null

比较常见的一个实现就是LinkedList(无界队列)。详细使用可见下图

在这里插入图片描述

BlockingQueue

对于阻塞队列BlockingQueue,因为继承了Queue接口,所以具备Queue的先进先出属性,但是也对一系列方法进行了增加了重写,常见方法有:
1、boolan add(E element) 往队尾插入元素,如果超出capacity,则抛出异常 注意,此方法不会阻塞 ps:和Queue方法含义一致
2、boolean offer(E element) 往队尾插入元素,成功返回true,失败(capacity restrictions)返回false 注意,此方法不会阻塞 ps:和Queue方法含义一致
3、void put(E element) 将一个元素加入到队列中,阻塞到直至加入成功为止,因为阻塞的语义,此方法可能抛出InterruptedException.
4、boolean offer(E element,long timeout,TimeUnit unit), 类似于put方法,但是只会阻塞指定时间,到时候没插入成功,则返回false
5、E take() 获取并弹出队头元素,阻塞直到成功为止
6、E poll(long timeout,TimeUnit uint) 获取并弹出队头元素,只会阻塞指定时间,如果时间到,则返回null

显然,3、4、5、6都是含有阻塞含义的方法,需要重点注意。

BlockingQueue实现类是LinkedBlockingQueue

通过阻塞队列如何实现线程复用?

我们回到线程池是如何通过阻塞队列实现复用上,我们知道,线程池的线程通过while()死循环,并阻塞在getTask()方法上来实现线程复用,所以我们继续分析getTask()方法
在这里插入图片描述
从上图可以知道,如果执行getTask时,当前时刻所有线程池的线程数不大于corePoolSize,那么当前执行getTask()方法的线程就会调用take()方法,一直阻塞,直到获取到任务后,才返回getTask()方法;而当前大于了corePoolSize,则调用poll()方法,只会阻塞keepAlive时间,如果仍旧没有获取到任务,则会返回getTask()方法为null并退出while死循环,最终该非核心线程会被ThreadPoolExecutor#processWorkerExit方法回收掉。PS:这里注意一个点,线程池中的线程并没有说最先创建出来的线程就是核心线程,后创建的就是非核心线程的说法,完全有可能threadpool-thread-1先被创建出来,但后来threadpool-thread-1执行getTask方法时候,判断到wokerCount > corePoolSize,那么这个线程虽然是第一个被创建出来的线程,也是会被回收掉。也就是说,核心线程是一个数量值,而不是一个boolean标志绑定在每个Worker上的。

take、poll等方法如何实现阻塞等待?

我们来分析一下LinkedBlockingQueue的源码,需要有AQS和Condition等基本了解,这些都是java并法包中的基础,如果了解Object wait() notify()这种线程间通讯的方式的话,理解起来并不难,可参考:AQS详解

LinkedBlockingQueue中有两个Lock和两个Condition变量

/** 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();

当往线程池中添加任务,workQueue.offer(command)时,执行一下代码:

public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }

当线程池的线程getTask()方法中执行workQueue.take()时,执行以下代码:

 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.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

从上述分析可知,offer和take使用的是两把锁,分别有自己的等待队列Condition。

offer方法源码分析:
1、如果添加元素时,已经等于队列的长度,此时不会阻塞当前线程,而是直接返回false,这也是线程池中队列满了之后,会直接创建非核心线程的关键所在
2、如果没达到容量,则需要先获取putLock,再次判断是否小于容量capacity,true时会将当前元素加入到队列中,并且调用notFull.signal()方法,告知其他的生产者,进行生产(主要是因为有些生产者可能调用put方法阻塞了,需要其他线程来进行唤醒操作)
3、最后调用if(c == 0) signalNotEmpty(); 注意这里c是getAndIncrement(),这种情况在什么时候发生?在当前队列为空时,当前生产者提交了一个任务,所以需要唤醒一个阻塞在本队列中其他消费者线程进行消费。这一步至关重要,因为线程池中的线程执行完firstTask任务后,就会调用workQueue.take()方法,如果队列中没任务则会一直阻塞,那么什么时候会被唤醒? 就是在此情况下,生产者生产了一个任务到队列时,由生产者线程来唤醒阻塞在队列上的消费者线程(当然了,take方法如果判断到队列元素大于1的话,本身也会唤醒其他消费者线程)

take方法源码分析:
1、尝试从队列中获取一个元素,如果队列为空,则当前消费者线程阻塞
2、如果当前队列不为空,那么从队列中弹出一个元素
3、判断当前容量是否大于1,如果大于1,需要调用notEmpty.signal()唤醒其他的消费者线程进行消费
4、如果c==capacity,注意这里是c = getAndDecrement,所以此时是队列元素 == capacity-1, 需要唤醒生产者线程进行生产(c < capacity-1的其他情况时,生产者线程会自行唤醒其他的生产者线程进行生产,无需消费者线程操心)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值