Java线程池中BlockingQueue的作用

原创 2018年04月16日 20:10:09

关于线程池中BlockingQueue的疑问

对于Java线程池,相信大家都或多或少使用过。关于其用法和原理介绍,网上已经有很多非常精彩的文章,珠玉在前,我就不献丑了。不了解的,可以参考这篇文章。今天我想讲的,是关于我对Java线程次的两个疑问,当然高手可以略过了。

  • 1.为什么线程池要使用BlockingQueue,而不是ArrayList或别的什么列表?
  • 2.既然使用了BlockingQueue,为什么还要设置拒绝策略,队列满的时候不是阻塞吗?

为什么使用阻塞队列?

要回答这个问答,首先来看看不用线程池的时候怎么执行异步任务

new Thread(() -> {
    // do something 
}).start();

也就是说,每次需要执行异步任务的时候,新建一个线程去执行,执行完就回收了。这会导致什么问题呢,首先,是对资源的浪费,线程的创建需要陷入内核,需要分配栈空间,需要执行调度,等等,只使用一次就回收太浪费资源。其次,当异步任务比较多的时候,这种方式要创建大量的线程,这对于内存资源也是一个很大的开销。我们知道,在jvm启动的时候可以设置线程栈大小的参数-Xss,默认的大小是1M,如果同时启动1000个线程,就要占用1G的内存,可想而知,这对内存是一个多大的开销。而且,线程数太多,对于内核的调度压力也是相当大的,而且,因为频繁的上下文切换而使程序的局部性丧失,也是一种消耗。线程池的作用,就是线程的复用,那么,怎么复用呢,来看一段代码:

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

ThreadPoolExecutor中,线程封装在Worker中,Worker实现了Runnable,同时在run()方法中调用上面的runWorker()方法,只要runWorker()方法没有执行完,这个线程就不会被回收。而runWorker()方法要执行下去,就要保证while (task != null || (task = getTask()) != null)的条件为真,第一次判断时task为firstTask,即执行的第一个任务,那么要点就成了getTask()必须不能为空,来看看getTask()的实现:

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

核心逻辑是:

Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();

这里的workQueue就是阻塞队列,timed表示是否会超时释放,keepAliveTime是非核心线程允许的空闲时间;如果不超时,则调用BlockingQueue.take(),如果取不到值,就会一直阻塞直到程序提交了一个任务。所以,阻塞队列的作用是控制线程池中线程的生命周期。

那么,如果不用阻塞队列,有没有别的方式可以实现线程池的功能?答案是,有,但是没必要。比如我们可以使用wait/notify来控制线程的执行和阻塞,但这里使用生产者/消费者模式来实现是一种更优雅的方式。

为什么需要拒绝策略

既然使用了阻塞队列,那添加任务的时候如果队列满了不就阻塞了吗,拒绝策略是干嘛用的?答案是添加任务调用的并不是阻塞的put()方法,而是非阻塞的offer()方法,看一下ThreadPoolExecutor的execute()方法就知道了

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

至于为什么这么实现,应该是不希望阻塞用户进程吧。

也就是说,在Java的线程池,只有消费者使用了阻塞的方法,生产者并没有。

SynchronousQueue

不过也有例外,调用ExecutorService executorService = Executors.newCachedThreadPool();
时,BlockingQueue的实现类是SynchronousQueue,顾名思义,这是一个同步队列,其内部没有容量,使用SynchronousQueue,消费者线程和生产者线程必须交替执行,也就是说,生产者和消费者都必须等待对方就绪。这样的话,不就阻塞用户进程了吗。确实会,但是这个时间非常短,因为使用这种方式,每次通过execute()提交任务的时候,要么复用现有空闲的线程,要么新建一个线程,也就是说线程数理论上没有上界,所以可以当作不会阻塞

参考资料

https://stackoverflow.com/questions/7556465/why-threadpoolexecutor-has-blockingqueue-as-its-argument?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

http://www.geek-programmer.com/java-blocking-queues-explained/

线程池.(Executors,ThreadPoolExecutor,BlockingQueue,RejectedExecutionHandler).介绍

线程池 Android里面,耗时的网络操作,都会开子线程,在程序里面直接开过多的线程会消耗过多的资源,在众多的开源框架中也总能看到线程池的踪影,所以线程池是必须要会把握的一个知识点; ...
  • Crystal_Plum9
  • Crystal_Plum9
  • 2016-04-27 19:42:39
  • 4038

ThreadPoolExecutor线程池解析与BlockingQueue的三种实现

目的主要介绍ThreadPoolExecutor的用法,和较浅显的认识,场景的使用方案等等,比较忙碌,如果有错误还请大家指出ThreadPoolExecutor介绍ThreadPoolExecutor...
  • a837199685
  • a837199685
  • 2016-02-01 16:31:13
  • 8139

java多线程-生产者消费者经典问题 基于BlockingQueue

java多线程-生产者消费者经典问题 基于BlockingQueue
  • xinyuan_java
  • xinyuan_java
  • 2016-08-04 11:26:38
  • 430

Java多线程之BlockingQueue深入分析

原文地址: http://www.2cto.com/kf/201212/175028.html        一、概述: BlockingQueue作为线程容器,可以为线程同步提供...
  • zenghb
  • zenghb
  • 2016-07-25 18:23:27
  • 1005

Java线程池中线程的状态简介

首先明确一下线程在JVM中的各个状态(JavaCore文件中) 1.死锁,Deadlock(重点关注)  2.执行中,Runnable(重点关注)    3.等待资源,Waiting...
  • libing13810124573
  • libing13810124573
  • 2015-12-21 09:36:06
  • 1340

java中线程队列BlockingQueue的用法

  • 2017年04月19日 10:22
  • 45KB
  • 下载

Java 线程池的异常处理机制

原文链接:https://my.oschina.net/lifany/blog/884002 一、前言 线程池技术是服务器端开发中常用的技术。不论是直接还是间接,各种服务器端功能的执行总是离不开线...
  • not_in_mountain
  • not_in_mountain
  • 2017-09-06 01:16:49
  • 1124

Java源码心中有数系列 BlockingQueue / BlockingDeque

Executors  简单的标准化接口 ExecutorService  提供了一个更加完整的异步任务 执行框架   管理任务的排队和调度 并允许受控关闭 Queues  队列 Timing ...
  • u010094934
  • u010094934
  • 2017-04-10 12:58:09
  • 798

ThreadPoolExecutor,worker和线程工厂之间理解

ThreadPoolExecutor中一个线程就是一个Worker对象,它与一个线程绑定,当Worker执行完毕就是线程执行完毕,这个在后面详细讨论线程池中线程的运行方式。而Worker带了锁,根据我...
  • xiaojiahao_kevin
  • xiaojiahao_kevin
  • 2016-06-25 20:55:26
  • 1604

BlockingQueue及其各个实现的分析整理

前面在整理的ThreadPoolExecutor类中,有一个从提交任务到线程池分配线程执行任务,有一个队列,而这个队列采用的就是BlockingQueue。BlockingQueue实际上定义了一个接...
  • kobejayandy
  • kobejayandy
  • 2015-07-10 18:35:20
  • 1822
收藏助手
不良信息举报
您举报文章:Java线程池中BlockingQueue的作用
举报原因:
原因补充:

(最多只允许输入30个字)