在面向对象编程中,创建和销毁对象是很耗时间和资源的,因此,在多线程编程过程中如果能减少线程的创建和销毁操作,那么对于程序的性能将得到很大的提高。Java线程池的出现,解决了这个问题。不仅如此,线程池还可以控制线程的创建数量,避免内存消耗过多。
学习线程池,需要用到以下几个关键类。
1、Executor
2、Executors
3、ExecutorSevice
4、ThreadPoolExecutor
在分别介绍它们之前,让我们先捋清一下它们之间的关系。
图中的AbstractExecutorService类可以不用理会,我们不讲它。另外,Excutors类不好在UML类图中将其涵盖进去,等会我们再介绍。
1 Executor
查看Executor的API,有这么一段话:
The Executor implementations provided in this package implement ExecutorService, which is a more extensive interface. The ThreadPoolExecutor class provides an extensible thread pool implementation. The Executors class provides convenient factory methods for these Executors.
大概意思是,ExecutorService接口在Executor接口的基础上又拓展了一些功能;而ThreadPoolExecutor类提供了一个可拓展的线程池实现;Executors类是一个工厂类,它为Executor类的实现提供了便捷的通道。
这里我们注意到了之前提到的Executors类,它是一个工厂方法,用来产生Executor对象。
1 | Executor excutor = Executors.newFixedThreadPool(10); |
注意以上是一个异步任务。同时,除了可以使用newFixedThreadPool创建Executor对象之外还可以使用
1 | Executors.newSingleThreadExecutor(); |
生产Executor对象,具体用法之后会介绍。
2 ExecutorSevice
如上所提,ExecutorSevice实际上只是比Executor多了一些方法而已,它同样可以通过Executors提供的工厂方法创造出来。
1 | ExecutorSevice executorService = Executors.newFixedThreadPool(10); |
除了execute(),ExecutorSevice提供了允许返回任务执行结果的submit(),返回结果的方式有两种:
1、传入Runnable对象。
1 | Future<String> future=executor.submit(new Runnable() { |
2、传入Callable对象。
1 | Future<String> future = executor.submit(new Callable<String>() { |
这个两种方式都可以通过future.get()获得返回的结果,并且future.get()是一个阻塞方法。不同的是,Runnable方式的返回值始终为null,Callable方式的返回值将取决于用户的设定。
特别地,ExecutorSevice还提供了可以处理Callable集合的方法invokeAny和invokeAll,具体用法可以参考这一篇文章。
将invokeAll运用在需要并行运行的场景,可以减少比较多的方法执行时间。比如:方法A和方法B分别都执行了一些耗时操作,而我们最后的值依赖于这两个方法的执行结果。那么第一种方法是让A、B串行执行,第二种方法是将A、B逻辑分别设置为callable对象,之后通过invokeAll并行执行它俩,实验结果是第二种方法优于第一种方法的实现。
3 ThreadPoolExecutor
ThreadPoolExecutor支持通过调整构造参数来配置不同的处理策略,下面主要介绍一下常用的策略配置方法以及应用场景。
3.1 ThreadPoolExecutor 的处理逻辑
首先看一下 ThreadPoolExecutor 构造函数的定义:
1 | public ThreadPoolExecutor(int corePoolSize, //线程池核心线程数量 |
ThreadPoolExecutor 对线程池和队列的使用方式如下:
1、从线程池中获取可用线程执行任务,如果没有可用线程则使用ThreadFactory创建新的线程,直到线程数达到corePoolSize限制;
2、线程池线程数达到corePoolSize以后,新的任务将被放入队列,直到队列不能再容纳更多的任务;
3、当队列不能再容纳更多的任务以后,会创建新的线程,直到线程数达到maxinumPoolSize限制;
4、线程数达到maxinumPoolSize限制以后新任务会被拒绝执行,调用 RejectedExecutionHandler 进行处理。
3.2 三种常用的 ThreadPoolExecutor
Executors 是提供了一组工厂方法用于创建常用的 ExecutorService ,分别是 FixedThreadPool,CachedThreadPool 以及 SingleThreadExecutor。这三种ThreadPoolExecutor都是调用 ThreadPoolExecutor 构造函数进行创建,区别在于参数不同。
1、FixedThreadPool - 线程池大小固定,任务队列无界。
下面是 Executors 类 newFixedThreadPool 方法的源码:
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
可以看到 corePoolSize 和 maximumPoolSize 设置成了相同的值,此时不存在线程数量大于核心线程数量的情况,所以KeepAlive时间设置不会生效。任务队列使用的是不限制大小的 LinkedBlockingQueue ,由于是无界队列所以容纳的任务数量没有上限。
因此,FixedThreadPool的行为如下:
1、从线程池中获取可用线程执行任务,如果没有可用线程则使用ThreadFactory创建新的线程,直到线程数达到nThreads;
2、线程池线程数达到nThreads以后,新的任务将被放入队列。
FixedThreadPool的优点是能够保证所有的任务都被执行,永远不会拒绝新的任务;同时缺点是队列数量没有限制,在任务执行时间无限延长的这种极端情况下会造成内存问题。
2、SingleThreadExecutor - 线程池大小固定为1,任务队列无界
1 | public static ExecutorService newSingleThreadExecutor() { |
这个工厂方法中使用无界LinkedBlockingQueue,并的将线程数设置成1。虽然是单线程处理,一旦线程因为处理异常等原因终止的时候,ThreadPoolExecutor会自动创建一个新的线程继续进行工作(Android程序还是会闪退的,Java程序才不会)。
SingleThreadExecutor 适用于在逻辑上需要单线程处理任务的场景,同时无界的LinkedBlockingQueue保证新任务都能够放入队列,不会被拒绝;缺点和FixedThreadPool相同,当处理任务无限等待的时候会造成内存问题。
3、CachedThreadPool - 线程池无限大(MAX INT),等待队列长度为1
1 | public static ExecutorService newCachedThreadPool() { |
SynchronousQueue是一个只有1个元素的队列,入队的任务需要一直等待直到队列中的元素被移出。核心线程数是0,意味着所有任务会先入队列;最大线程数是Integer.MAX_VALUE,可以认为线程数量是没有限制的。KeepAlive时间被设置成60秒,意味着在没有任务的时候超过核心线程数的线程等待60秒以后退出。CachedThreadPool对任务的处理策略是提交的任务会立即分配一个线程进行执行,线程池中线程数量会随着任务数的变化自动扩张和缩减,在任务执行时间无限延长的极端情况下会创建过多的线程。
3.3 Java线程池中 BlockingQueue 的作用
原文链接
注意,参考文章最后对 SynchronousQueue 阻塞相关的介绍需要更正。它在execute方法中并不会导致主线程阻塞,而是和 LinkedBlockingQueue 一样,会调用 BlockingQueue 的boolean offer(E e)方法,该方法不是阻塞的。
3.4 ExecutorService的关闭
当我们使用完成ExecutorService之后应该主动关闭它,否则它里面的线程大多数情况会一直处于运行状态。(虽然通过设置 allowCoreThreadTimeOut == true 或者 超过核心线程数的线程等待 KeepAlive 会自动结束,但还是建议主动关闭线程池。)
如果要关闭ExecutorService中执行的线程,我们可以调用ExecutorService.shutdown()方法。在调用shutdown()方法之后,ExecutorService不会立即关闭,但是它不再接收新的任务,直到当前所有线程执行完成才会关闭,所有在shutdown()执行之前提交的任务都会被执行。
如果我们想立即关闭ExecutorService,我们可以调用ExecutorService.shutdownNow()方法。这个动作将跳过所有正在执行的任务和被提交还没有执行的任务。但是它并不对正在执行的任务做任何保证,有可能它们都会停止,也有可能执行完成。如:线程正在执行一个循环任务,那么想要在调用ExecutorService.shutdownNow()后响应最新的状态,就需要在线程中实时获取 isInterrupted() 进行判断了。
注意:循环任务中如果有 sleep | wait 的阻塞被外界打断,那么在打断后 isInterrupted() 的结果将会重置为 false 。
4 补充
4.1 多线程实现汇总处理的思路
1、获取线程池的 Future 。
2、线程池添加任务后,shutdown 和 awaitTermination 配合。
3、通过 Thread.join() 方式。
4、kotlin 协程中 async 方式。
4.2 多线程实现顺序调用的思路
1、配置线程池的核心线程数与总线程数为1,等待队列无穷大 。
2、通过 wait notifyAll 配合。
3、通过 Thread.join() 方式。
4.3 java 实现线程的方式
1、继承 Thread,重写 run 方法。
2、继承 Runnable,重写 run 方法,将 Runnable 对象传递到 Thread。
3、方式 1 和 2 的匿名内部类实现。
4、线程池。
5、Timer。
4.4 yeild方法的作用
暂时放弃 CPU 的执行时间,和其他线程重新竞争 CPU 的执行机会。参考文章>>
4.5 final是如何保证线程可见性的
4.6 线程被阻塞的几种情况
1、调用了 wait/sleep方法。
2、io阻塞。
3、等待获取锁资源。