Java多线程研究04-线程池的使用(ThreadPoolExecutor详解)

为什么要使用线程池?

线程是一个操作系统概念。操作系统负责这个线程的创建、挂起、运行、阻塞和终结操作。而操作系统创建线程、切换线程状态、终结线程都要进行CPU调度——这是一个耗费时间和系统资源的事情。

另一方面,大多数实际场景中是这样的:处理某一次请求的时间是非常短暂的,但是请求数量是巨大的。这种技术背景下,如果我们为每一个请求都单独创建一个线程,那么物理机的所有资源基本上都被操作系统创建线程、切换线程状态、销毁线程这些操作所占用,用于业务请求处理的资源反而减少了。所以最理想的处理方式是,将处理请求的线程数量控制在一个范围,既保证后续的请求不会等待太长时间,又保证物理机将足够的资源用于请求处理本身。

另外,一些操作系统是有最大线程数量限制的。当运行的线程数量逼近这个值的时候,操作系统会变得不稳定。这也是我们要限制线程数量的原因。

线程池的基本使用方式

JAVA语言为我们提供了两种基础线程池的选择:ScheduledThreadPoolExecutor和ThreadPoolExecutor。它们都实现了ExecutorService接口(注意,ExecutorService接口本身和“线程池”并没有直接关系,它的定义更接近“执行器”,而“使用线程管理的方式进行实现”只是其中的一种实现方式)。这篇文章中,我们主要围绕ThreadPoolExecutor类进行讲解。

ThreadPoolExecutor类的使用方式:

public class PoolThreadSimple {

    public static void main(String[] args) throws Throwable {

        /*
         * corePoolSize:核心大小,线程池初始化的时候,就会有这么大
         * maximumPoolSize:线程池最大线程数
         * keepAliveTime:如果当前线程池中线程数大于corePoolSize。
         * 多余的线程,在等待keepAliveTime时间后如果还没有新的线程任务指派给它,它就会被回收
         *
         * unit:等待时间keepAliveTime的单位
         *
         * workQueue:等待队列。这个对象的设置是本文将重点介绍的内容
         * */
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MINUTES, new SynchronousQueue<Runnable>());
        for(int index = 0 ; index < 10 ; index ++) {
            poolExecutor.submit(new PoolThreadSimple.TestRunnable(index));
        }

    }

    /**
     * 这个就是测试用的线程
     */
    private static class TestRunnable implements Runnable {

        /**
         * 日志
         */
        private static Log LOGGER = LogFactory.getLog(TestRunnable.class);

        /**
         * 记录任务的唯一编号,这样在日志中好做识别
         */
        private Integer index;

        public TestRunnable(int index) {
            this.index = index;
        }

        /**
         * @return the index
         */
        public Integer getIndex() {
            return index;
        }

        @Override
        public void run() {
            /*
             * 线程中,就只做一件事情:
             * 等待60秒钟的事件,以便模拟业务操作过程
             * */
            Thread currentThread  = Thread.currentThread();
            TestRunnable.LOGGER.info("线程:" + currentThread.getId() + " 中的任务(" + this.getIndex() + ")开始执行===");
            synchronized (currentThread) {
                try {
                    currentThread.wait(60000);
                } catch (InterruptedException e) {
                    TestRunnable.LOGGER.error(e.getMessage(), e);
                }
            }

            TestRunnable.LOGGER.info("线程:" + currentThread.getId() + " 中的任务(" + this.getIndex() + ")执行完成");
        }

    }
}

下文中,将对线程池中的corePoolSize、maximumPoolSize、keepAliveTime、timeUnit、workQueue、threadFactory、handler参数和一些常用/不常用的设置项进行逐一讲解。

ThreadPoolExecutor逻辑结构和工作方式

在上面的代码中,我们创建线程池的时候使用了ThreadPoolExecutor中最简单的一个构造函数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)

构造函数中需要传入的参数包括corePoolSize、maximumPoolSize、keepAliveTime、timeUnit和workQueue。要明确理解这些参数(和后续将要介绍的参数)的含义,就首先要搞清楚ThreadPoolExecutor线程池的逻辑结构。

一定要注意一个概念,即存在于线程池中容器的一定是Thread对象,而不是你要求运行的任务(所以叫线程池而不叫任务池也不叫对象池);你要求运行的任务将被线程池分配给某一个空闲的Thread运行。

从上图中,我们可以看到构成线程池的几个重要元素:

等待队列:顾名思义,就是你调用线程池对象的submit()方法或者execute()方法,要求线程池运行的任务(这些任务必须实现Runnable接口或者Callable接口)。但是出于某些原因线程池并没有马上运行这些任务,而是送入一个队列等待执行。

核心线程:线程池主要用于执行任务的是“核心线程”,“核心线程”的数量是你创建线程时所设置的corePoolSize参数决定的。如果不进行特别的设定,线程池中始终会保持corePoolSize数量的线程数(不包括创建阶段)。

非核心线程:一旦任务数量过多(由等待队列的特性决定),线程池将创建“非核心线程”临时帮助运行任务。你设置的大于corePoolSize参数小于maximumPoolSize参数的部分,就是线程池可以临时创建的“非核心线程”的最大数量。这种情况下如果某个线程没有运行任何任务,在等待keepAliveTime时间后,这个线程将会被销毁,直到线程池的线程数量重新达到corePoolSize。

maximumPoolSize参数也是当前线程池允许创建的最大线程数量。那么如果设置的corePoolSize参数和设置的maximumPoolSize参数一致时,线程池在任何情况下都不会回收空闲线程。keepAliveTime和timeUnit也就失去了意义。

keepAliveTime参数和timeUnit参数也是配合使用的。keepAliveTime参数指明等待时间的量化值,timeUnit指明量化值单位。例如keepAliveTime=1,timeUnit为TimeUnit.MINUTES,代表空闲线程的回收阀值为1分钟。

说完了线程池的逻辑结构,下面我们讨论一下线程池是怎样处理某一个运行任务的。

1、首先可以通过线程池提供的submit()方法或者execute()方法,要求线程池执行某个任务。线程池收到这个要求执行的任务后,会有几种处理情况:

1.1、如果当前线程池中运行的线程数量还没有达到corePoolSize大小时,线程池会创建一个新的线程运行你的任务,无论之前已经创建的线程是否处于空闲状态。

1.2、如果当前线程池中运行的线程数量已经达到设置的corePoolSize大小,线程池会把你的这个任务加入到等待队列中。直到某一个的线程空闲了,线程池会根据设置的等待队列规则,从队列中取出一个新的任务执行。

1.3、如果根据队列规则,这个任务无法加入等待队列。这时线程池就会创建一个“非核心线程”直接运行这个任务。注意,如果这种情况下任务执行成功,那么当前线程池中的线程数量一定大于corePoolSize。

1.4、如果这个任务,无法被“核心线程”直接执行,又无法加入等待队列,又无法创建“非核心线程”直接执行,且你没有为线程池设置RejectedExecutionHandler。这时线程池会抛出RejectedExecutionException异常,即线程池拒绝接受这个任务。(实际上抛出RejectedExecutionException异常的操作,是ThreadPoolExecutor线程池中一个默认的RejectedExecutionHandler实现:AbortPolicy,这在后文会提到)

2、一旦线程池中某个线程完成了任务的执行,它就会试图到任务等待队列中拿去下一个等待任务(所有的等待任务都实现了BlockingQueue接口,按照接口字面上的理解,这是一个可阻塞的队列接口),它会调用等待队列的poll()方法,并停留在哪里。

3、当线程池中的线程超过你设置的corePoolSize参数,说明当前线程池中有所谓的“非核心线程”。那么当某个线程处理完任务后,如果等待keepAliveTime时间后仍然没有新的任务分配给它,那么这个线程将会被回收。线程池回收线程时,对所谓的“核心线程”和“非核心线程”是一视同仁的,直到线程池中线程的数量等于你设置的corePoolSize参数时,回收过程才会停止。

不常用的设置

在ThreadPoolExecutor线程池中,有一些不常用的甚至不需要的设置

allowCoreThreadTimeOut:

线程池回收线程只会发生在当前线程池中线程数量大于corePoolSize参数的时候;当线程池中线程数量小于等于corePoolSize参数的时候,回收过程就会停止。

allowCoreThreadTimeOut设置项可以要求线程池:将包括“核心线程”在内的,没有任务分配的任何线程,在等待keepAliveTime时间后全部进行回收:

ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1));

poolExecutor.allowCoreThreadTimeOut(true);

以下是设置前的效果:

以下是设置后的效果:

prestartAllCoreThreads

前文我们还讨论到,当线程池中的线程还没有达到你设置的corePoolSize参数值的时候,如果有新的任务到来,线程池将创建新的线程运行这个任务,无论之前已经创建的线程是否处于空闲状态。这个描述可以用下面的示意图表示出来:

prestartAllCoreThreads设置项,可以在线程池创建,但还没有接收到任何任务的情况下,先行创建符合corePoolSize参数值的线程数:

ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1));

poolExecutor.prestartAllCoreThreads();
时间: 2024-08-03 17:11:58

Java多线程研究04-线程池的使用(ThreadPoolExecutor详解)的相关文章

Java常用四大线程池用法以及ThreadPoolExecutor详解

为什么用线程池? 1.创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处-理效率 2.线程并发数量过多,抢占系统资源从而导致阻塞 3.对线程进行一些简单的管理 在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类,学习Java中的线程池,就可以直接学习他了对线程池的配置,就是对ThreadPoolExecutor构造函数的参数的配置 一.ThreadPoolExecutor提供了四个构造函数: //五个参数的构造函数 pu

Java多线程-新特性-线程池

Sun在Java5中,对Java线程的类库做了大量的扩展,其中线程池就是Java5的新特征之一,除了线程池之外,还有很多多线程相关的内容,为多线程的编程带来了极大便利.为了编写高效稳定可靠的多线程程序,线程部分的新增内容显得尤为重要. 有关Java5线程新特征的内容全部在java.util.concurrent下面,里面包含数目众多的接口和类,熟悉这部分API特征是一项艰难的学习过程.目前有关这方面的资料和书籍都少之又少,大部分介绍线程方面书籍还停留在java5之前的知识层面上. 在Java5之

Java多线程——TreadPoolExecutor,线程池

一.为什么使用线程池: 1.降低资源损耗. 主要指的是降低创建和销毁线程时产生的cpu资源消耗,线程池通过持续工作的线程执行不断分配的新     任务,来减少频繁的线程创建与销毁. 2.提高响应速度. 3.提高线程的可管理性. 线程是稀缺资源,频繁的创建销毁,以及没有控制的大量创建,都会影响系统的稳定性.使用线程池可以统一分配,调优,资源监控. 二.如何创建线程池: new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, lon

Java多线程系列--“JUC线程池”04之 线程池原理(三)

本章介绍线程池的生命周期. 线程有5种状态:新建状态,就绪状态,运行状态,阻塞状态,死亡状态.线程池也有5种状态:然而,线程池不同于线程,线程池的5种状态是:Running, SHUTDOWN, STOP, TIDYING, TERMINATED. 线程池状态定义代码如下: /** * The main pool control state, ctl, is an atomic integer packing * two conceptual fields * workerCount, indi

Java多线程系列--“JUC线程池”01之 线程池架构

概要 前面分别介绍了"Java多线程基础"."JUC原子类"和"JUC锁".本章介绍JUC的最后一部分的内容——线程池.内容包括:线程池架构图线程池示例 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3509903.html 线程池架构图 线程池的架构图如下: 1. Executor 它是"执行者"接口,它是来执行任务的.准确的说,Executor提供了execute()接口来执行

Java多线程设计模式(4)线程池模式

前序: Thread-Per-Message Pattern,是一种对于每个命令或请求,都分配一个线程,由这个线程执行工作.它将“委托消息的一端”和“执行消息的一端”用两个不同的线程来实现.该线程模式主要包括三个部分: 1,Request参与者(委托人),也就是消息发送端或者命令请求端 2,Host参与者,接受消息的请求,负责为每个消息分配一个工作线程. 3,Worker参与者,具体执行Request参与者的任务的线程,由Host参与者来启动. 由于常规调用一个方法后,必须等待该方法完全执行完毕

Java多线程系列--“JUC线程池”03之 线程池原理(二)

线程池示例 在分析线程池之前,先看一个简单的线程池示例. import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; public class ThreadPoolDemo1 { public static void main(String[] args) { // 创建一个可重用固定线程数的线程池 ExecutorService pool = Executors.newFixedThre

Java多线程系列--“JUC线程池”05之 线程池原理(四)

拒绝策略介绍 线程池的拒绝策略,是指当任务添加到线程池中被拒绝,而采取的处理措施.当任务添加到线程池中之所以被拒绝,可能是由于:第一,线程池异常关闭.第二,任务数量超过线程池的最大限制. 线程池共包括4种拒绝策略,它们分别是:AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy和DiscardPolicy. AbortPolicy -- 当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常. Calle

Java多线程——Executors和线程池

线程池的概念与Executors类的应用 1.创建固定大小的线程池 package java_thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class ThreadPoolTest { /** * @param args */ public static void mai

Java多线程系列--“JUC线程池”06之 Callable和Future

概要 本章介绍线程池中的Callable和Future.Callable 和 Future 简介示例和源码分析(基于JDK1.7.0_40) 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3544116.html Callable 和 Future 简介 Callable 和 Future 是比较有趣的一对组合.当我们需要获取线程的执行结果时,就需要用到它们.Callable用于产生结果,Future用于获取结果. 1. Callable Calla