Java编程的逻辑 (78) - 线程池
本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http://item.jd.com/12299018.html

上节,我们初步探讨了Java并发包中的任务执行服务,实际中,任务执行服务的主要实现机制是线程池,本节,我们就来探讨线程池。
基本概念
线程池,顾名思义,就是一个线程的池子,里面有若干线程,它们的目的就是执行提交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行新任务。线程池主要由两个概念组成,一个是任务队列,另一个是工作者线程,工作者线程主体就是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务。
线程池的概念类似于生活中的一些排队场景,比如在火车站排队购票、在医院排队挂号、在银行排队办理业务等,一般都由若干个窗口提供服务,这些服务窗口类似于工作者线程,而队列的概念是类似的,只是,在现实场景中,每个窗口经常有一个单独的队列,这种排队难以公平,随着信息化的发展,越来越多的排队场合使用虚拟的统一队列,一般都是先拿一个排队号,然后按号依次服务。
线程池的优点是显而易见的:
- 它可以重用线程,避免线程创建的开销
 - 在任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成
 
Java并发包中线程池的实现类是ThreadPoolExecutor,它继承自AbstractExecutorService,实现了ExecutorService,基本用法与上节介绍的类似,我们就不赘述了。不过,ThreadPoolExecutor有一些重要的参数,理解这些参数对于合理使用线程池非常重要,接来下,我们探讨这些参数。
理解线程池
构造方法
ThreadPoolExecutor有多个构造方法,都需要一些参数,主要构造方法有:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
第二个构造方法多了两个参数threadFactory和handler,这两个参数一般不需要,第一个构造方法会设置默认值。
参数corePoolSize, maximumPoolSize, keepAliveTime, unit用于控制线程池中线程的个数,workQueue表示任务队列,threadFactory用于对创建的线程进行一些配置,handler表示任务拒绝策略。下面我们再来详细探讨下这些参数。
线程池大小
线程池的大小主要与四个参数有关:
- corePoolSize:核心线程个数
 - maximumPoolSize:最大线程个数
 - keepAliveTime和unit:空闲线程存活时间
 
maximumPoolSize表示线程池中的最多线程数,线程的个数会动态变化,但这是最大值,不管有多少任务,都不会创建比这个值大的线程个数。
corePoolSize表示线程池中的核心线程个数,不过,这并不是说,一开始就创建这么多线程,刚创建一个线程池后,实际上并不会创建任何线程。
一般情况下,有新任务到来的时候,如果当前线程个数小于corePoolSiz,就会创建一个新线程来执行该任务,需要说明的是,即使其他线程现在也是空闲的,也会创建新线程。
不过,如果线程个数大于等于corePoolSiz,那就不会立即创建新线程了,它会先尝试排队,需要强调的是,它是"尝试"排队,而不是"阻塞等待"入队,如果队列满了或其他原因不能立即入队,它就不会排队,而是检查线程个数是否达到了maximumPoolSize,如果没有,就会继续创建线程,直到线程数达到maximumPoolSize。
keepAliveTime的目的是为了释放多余的线程资源,它表示,当线程池中的线程个数大于corePoolSize时,额外空闲线程的存活时间,也就是说,一个非核心线程,在空闲等待新任务时,会有一个最长等待时间,即keepAliveTime,如果到了时间还是没有新任务,就会被终止。如果该值为0,表示所有线程都不会超时终止。
这几个参数除了可以在构造方法中进行指定外,还可以通过getter/setter方法进行查看和修改。
public void setCorePoolSize(int corePoolSize)
public int getCorePoolSize()
public int getMaximumPoolSize()
public void setMaximumPoolSize(int maximumPoolSize)
public long getKeepAliveTime(TimeUnit unit)
public void setKeepAliveTime(long time, TimeUnit unit)
除了这些静态参数,ThreadPoolExecutor还可以查看关于线程和任务数的一些动态数字:
//返回当前线程个数
public int getPoolSize()
//返回线程池曾经达到过的最大线程个数
public int getLargestPoolSize()
//返回线程池自创建以来所有已完成的任务数
public long getCompletedTaskCount()
//返回所有任务数,包括所有已完成的加上所有排队待执行的
public long getTaskCount()
队列
ThreadPoolExecutor要求的队列类型是阻塞队列BlockingQueue,我们在76节介绍过多种BlockingQueue,它们都可以用作线程池的队列,比如:
- LinkedBlockingQueue:基于链表的阻塞队列,可以指定最大长度,但默认是无界的。
 - ArrayBlockingQueue:基于数组的有界阻塞队列
 - PriorityBlockingQueue:基于堆的无界阻塞优先级队列
 - SynchronousQueue:没有实际存储空间的同步阻塞队列
 
如果用的是无界队列,需要强调的是,线程个数最多只能达到corePoolSize,到达corePoolSize后,新的任务总会排队,参数maximumPoolSize也就没有意义了。
另一面,对于SynchronousQueue,我们知道,它没有实际存储元素的空间,当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到达到maximumPoolSize。
任务拒绝策略
如果队列有界,且maximumPoolSize有限,则当队列排满,线程个数也达到了maximumPoolSize,这时,新任务来了,如何处理呢?此时,会触发线程池的任务拒绝策略。
默认情况下,提交任务的方法如execute/submit/invokeAll等会抛出异常,类型为RejectedExecutionException。
不过,拒绝策略是可以自定义的,ThreadPoolExecutor实现了四种处理方式:
- ThreadPoolExecutor.AbortPolicy:这就是默认的方式,抛出异常
 - ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛异常,也不执行
 - ThreadPoolExecutor.DiscardOldestPolicy:将等待时间最长的任务扔掉,然后自己排队
 - ThreadPoolExecutor.CallerRunsPolicy:在任务提交者线程中执行任务,而不是交给线程池中的线程执行
 
它们都是ThreadPoolExecutor的public静态内部类,都实现了RejectedExecutionHandler接口,这个接口的定义为:
public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
当线程池不能接受任务时,调用其拒绝策略的rejectedExecution方法。
拒绝策略可以在构造方法中进行指定,也可以通过如下方法进行指定:
public void setRejectedExecutionHandler(RejectedExecutionHandler handler)
默认的RejectedExecutionHandler是一个AbortPolicy实例,如下所示:
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
而AbortPolicy的rejectedExecution实现就是抛出异常,如下所示:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}
我们需要强调下,拒绝策略只有在队列有界,且maximumPoolSize有限的情况下才会触发。
如果队列无界,服务不了的任务总是会排队,但这不见得是期望的,因为请求处理队列可能会消耗非常大的内存,甚至引发内存不够的异常。
如果队列有界但maximumPoolSize无限,可能会创建过多的线程,占满CPU和内存,使得任何任务都难以完成。
所以,在任务量非常大的场景中,让拒绝策略有机会执行是保证系统稳定运行很重要的方面。
线程工厂
线程池还可以接受一个参数,ThreadFactory,它是一个接口,定义为:
public interface ThreadFactory {
    Thread newThread(Runnable r);
}
这个接口根据Runnable创建一个Thread,ThreadPoolExecutor的默认实现是Executors类中的静态内部类DefaultThreadFactory,主要就是创建一个线程,给线程设置一个名称,设置daemon属性为false,设置线程优先级为标准默认优先级,线程名称的格式为: pool-<线程池编号>-thread-<线程编号>。
如果需要自定义一些线程的属性,比如名称,可以实现自定义的ThreadFactory。
关于核心线程的特殊配置
线程个数小于等于corePoolSize时,我们称这些线程为核心线程,默认情况下:
- 核心线程不会预先创建,只有当有任务时才会创建
 - 核心线程不会因为空闲而被终止,keepAliveTime参数不适用于它
 
不过,ThreadPoolExecutor有如下方法,可以改变这个默认行为。
//预先创建所有的核心线程
public int prestartAllCoreThreads()
//创建一个核心线程,如果所有核心线程都已创建,返回false
public boolean prestartCoreThread()
//如果参数为true,则keepAliveTime参数也适用于核心线程
public void allowCoreThreadTimeOut(boolean value)
工厂类Executors
类Executors提供了一些静态工厂方法,可以方便的创建一些预配置的线程池,主要方法有:
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newCachedThreadPool()
newSingleThreadExecutor基本相当于调用:
public static ExecutorService newSingleThreadExecutor() {
    return new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());
}
只使用一个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止,该线程顺序执行所有任务。该线程池适用于需要确保所有任务被顺序执行的场合。
newFixedThreadPool的代码为:
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
使用固定数目的n个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止。和newSingleThreadExecutor一样,由于是无界队列,如果排队任务过多,可能会消耗非常大的内存。
newCachedThreadPool的代码为:
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
它的corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,keepAliveTime是60秒,队列为SynchronousQueue。
它的含义是,当新任务到来时,如果正好有空闲线程在等待任务,则其中一个空闲线程接受该任务,否则就总是创建一个新线程,创建的总线程个数不受限制,对任一空闲线程,如果60秒内没有新任务,就终止。
实际中,应该使用newFixedThreadPool还是newCachedThreadPool呢?
在系统负载很高的情况下,newFixedThreadPool可以通过队列对新任务排队,保证有足够的资源处理实际的任务,而newCachedThreadPool会为每个任务创建一个线程,导致创建过多的线程竞争CPU和内存资源,使得任何实际任务都难以完成,这时,newFixedThreadPool更为适用。
不过,如果系统负载不太高,单个任务的执行时间也比较短,newCachedThreadPool的效率可能更高,因为任务可以不经排队,直接交给某一个空闲线程。
在系统负载可能极高的情况下,两者都不是好的选择,newFixedThreadPool的问题是队列过长,而newCachedThreadPool的问题是线程过多,这时,应根据具体情况自定义ThreadPoolExecutor,传递合适的参数。
线程池的死锁
关于提交给线程池的任务,我们需要特别注意一种情况,就是任务之间有依赖,这种情况可能会出现死锁。比如任务A,在它的执行过程中,它给同样的任务执行服务提交了一个任务B,但需要等待任务B结束。
如果任务A是提交给了一个单线程线程池,就会出现死锁,A在等待B的结果,而B在队列中等待被调度。
如果是提交给了一个限定线程个数的线程池,也有可能出现死锁,我们看个简单的例子:
public class ThreadPoolDeadLockDemo {
    private static final int THREAD_NUM = 5;
    static ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
    static class TaskA implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Future<?> future = executor.submit(new TaskB());
            try {
                future.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("finished task A");
        }
    }
    static class TaskB implements Runnable {
        @Override
        public void run() {
            System.out.println("finished task B");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            executor.execute(new TaskA());
        }
        Thread.sleep(2000);
        executor.shutdown();
    }
}
以上代码使用newFixedThreadPool创建了一个5个线程的线程池,main程序提交了5个TaskA,TaskA会提交一个TaskB,然后等待TaskB结束,而TaskB由于线程已被占满只能排队等待,这样,程序就会死锁。
怎么解决这种问题呢?
替换newFixedThreadPool为newCachedThreadPool,让创建线程不再受限,这个问题就没有了。
另一个解决方法,是使用SynchronousQueue,它可以避免死锁,怎么做到的呢?对于普通队列,入队只是把任务放到了队列中,而对于SynchronousQueue来说,入队成功就意味着已有线程接受处理,如果入队失败,可以创建更多线程直到maximumPoolSize,如果达到了maximumPoolSize,会触发拒绝机制,不管怎么样,都不会死锁。我们将创建executor的代码替换为:
static ExecutorService executor = new ThreadPoolExecutor(
THREAD_NUM, THREAD_NUM, 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
只是更改队列类型,运行同样的程序,程序不会死锁,不过TaskA的submit调用会抛出异常RejectedExecutionException,因为入队会失败,而线程个数也达到了最大值。
小结
本节介绍了线程池的基本概念,详细探讨了其主要参数的含义,理解这些参数对于合理使用线程池是非常重要的,对于相互依赖的任务,需要特别注意,避免出现死锁。
ThreadPoolExecutor实现了生产者/消费者模式,工作者线程就是消费者,任务提交者就是生产者,线程池自己维护任务队列。当我们碰到类似生产者/消费者问题时,应该优先考虑直接使用线程池,而非重新发明轮子,自己管理和维护消费者线程及任务队列。
在异步任务程序中,一种常见的场景是,主线程提交多个异步任务,然后有任务完成就处理结果,并且按任务完成顺序逐个处理,对于这种场景,Java并发包提供了一个方便的方法,使用CompletionService,让我们下一节来探讨它。
(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic)
----------------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (78) - 线程池的更多相关文章
- Java编程的逻辑 (65) - 线程的基本概念
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - Java编程的逻辑 (67) - 线程的基本协作机制 (上)
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - Java编程的逻辑 (68) - 线程的基本协作机制 (下)
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - Java编程的逻辑 (69) - 线程的中断
		
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
 - 《Java编程的逻辑》 - 文章列表
		
<计算机程序的思维逻辑>系列文章已整理成书<Java编程的逻辑>,由机械工业出版社出版,2018年1月上市,各大网店有售,敬请关注! 京东自营链接:https://item.j ...
 - Java 并发编程——Executor框架和线程池原理
		
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
 - Java 并发编程——Executor框架和线程池原理
		
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
 - Java并发编程:4种线程池和缓冲队列BlockingQueue
		
一. 线程池简介 1. 线程池的概念: 线程池就是首先创建一些线程,它们的集合称为线程池.使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动 ...
 - [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors
		
[Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...
 
随机推荐
- shell seq 用法
			
seq [OPTION]... LASTseq [OPTION]... FIRST LASTseq [OPTION]... FIRST INCREMENT LAST seq 1000 ‘起始默认是 ...
 - WinForm中DataGridView导出为Excel(快速版)
			
public static void ExportExcel(DataGridView myDGV, string fileName) { string saveFileName = fileName ...
 - IT 产品 需求 痛点
			
英文应该有个 汉语发音 标注 这样的平台软件.罗马音.
 - 【数据结构与算法】自己动手实现图的BFS和DFS(附完整源码)
			
转载请注明出处:http://blog.csdn.net/ns_code/article/details/19617187 图的存储结构 本文的重点在于图的深度优先搜索(DFS)和广度优先搜索(BFS ...
 - 设置Sublime Text 3的光标样式
			
升级了Sublime Text 3,结果光标变成了这个样子,非常不习惯: 查了文档http://www.sublimetext.com/3 ,Build 3059中得描述: Added setting ...
 - ORACLE 内置函数之 GREATEST 和 LEAST(转)
			
Oracle比较一列的最大值或者最小值,我们会不假思索地用MAX和MIN函数,但是对于比较一行的最大值或最小值呢?是不是日常用的少,很多人都不知道有ORACLE也有内置函数实现这个功能:COALESC ...
 - C#高级编程9-第13章 异步编程
			
异步编程 1)异步编程的重要性 在C#5.0中提供了关键字:async和await 使用异步编程后台运行方法调用,程序的运行过程中就不会一直处于等待中.便于用户继续操作. 异步编程有3种模式:异步模式 ...
 - spring cloud——feign为GET请求时的对象参数传递
			
一.问题重现 楼主在使用feign进行声明式服务调用的时候发现,当GET请求为多参数时,为方便改用DTO对象进行参数传递.但是,在接口调用时feign会抛出一个405的请求方式错误: {"t ...
 - 熬之滴水成石:最想深入了解的内容--windows内核机制(6)
			
58 进程和线程(3) 说完进程再说说线程,线程相比于进程其实有更多可说的内容.首先实现线程调用的数据结构是个栈,该栈记录了调用方法的信息这里面也包括了函数调用及返回的地址.线程肯定是属于某个进程,其 ...
 - 每天一个linux命令(2):cd命令
			
1.作用 cd(Change Directory 改变目录)命令用来切换工作目录至dirname. 其中dirName表示法可为绝对路径或相对路径.若目录名称省略,则变换至使用者的home direc ...