Java线程池

Catalogue
  1. 1 Executor
  2. 2 ExecutorSevice
  3. 3 ThreadPoolExecutor
    1. 3.1 ThreadPoolExecutor 的处理逻辑
    2. 3.2 三种常用的 ThreadPoolExecutor
    3. 3.3 Java线程池中 BlockingQueue 的作用
    4. 3.4 ExecutorService的关闭
  4. 4 补充
    1. 4.1 多线程实现汇总处理的思路
    2. 4.2 多线程实现顺序调用的思路
    3. 4.3 java 实现线程的方式
    4. 4.4 yeild方法的作用
    5. 4.5 final是如何保证线程可见性的
    6. 4.6 线程被阻塞的几种情况
    7. 4.7 线程的生命周期
  5. 参考资料

在面向对象编程中,创建和销毁对象是很耗时间和资源的,因此,在多线程编程过程中如果能减少线程的创建和销毁操作,那么对于程序的性能将得到很大的提高。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
2
3
4
5
6
Executor excutor = Executors.newFixedThreadPool(10);  
excutor.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});

注意以上是一个异步任务。同时,除了可以使用newFixedThreadPool创建Executor对象之外还可以使用

1
2
Executors.newSingleThreadExecutor();  
Executors.newScheduledThreadPool(size);

生产Executor对象,具体用法之后会介绍。

2 ExecutorSevice

如上所提,ExecutorSevice实际上只是比Executor多了一些方法而已,它同样可以通过Executors提供的工厂方法创造出来。

1
ExecutorSevice executorService = Executors.newFixedThreadPool(10);  

除了execute(),ExecutorSevice提供了允许返回任务执行结果的submit(),返回结果的方式有两种:
1、传入Runnable对象。

1
2
3
4
5
6
Future<String> future=executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("Asynchronous task");
}
});

2、传入Callable对象。

1
2
3
4
5
6
7
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("Asynchronous task");
return "result";
}
});

这个两种方式都可以通过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
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,  //线程池核心线程数量
int maximumPoolSize, //线程池最大线程数量
long keepAliveTime, //线程KeepAlive时间,当线程池数量超过核心线程数量以后,idle时间超过这个值的线程会被终止
TimeUnit unit, //线程KeepAlive时间单位
BlockingQueue<Runnable> workQueue, //任务队列
ThreadFactory threadFactory, //创建线程的工厂对象
RejectedExecutionHandler handler) //任务被拒绝后调用的handler

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
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

可以看到 corePoolSize 和 maximumPoolSize 设置成了相同的值,此时不存在线程数量大于核心线程数量的情况,所以KeepAlive时间设置不会生效。任务队列使用的是不限制大小的 LinkedBlockingQueue ,由于是无界队列所以容纳的任务数量没有上限。
因此,FixedThreadPool的行为如下:
1、从线程池中获取可用线程执行任务,如果没有可用线程则使用ThreadFactory创建新的线程,直到线程数达到nThreads;
2、线程池线程数达到nThreads以后,新的任务将被放入队列。
FixedThreadPool的优点是能够保证所有的任务都被执行,永远不会拒绝新的任务;同时缺点是队列数量没有限制,在任务执行时间无限延长的这种极端情况下会造成内存问题。
2、SingleThreadExecutor - 线程池大小固定为1,任务队列无界

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

这个工厂方法中使用无界LinkedBlockingQueue,并的将线程数设置成1。虽然是单线程处理,一旦线程因为处理异常等原因终止的时候,ThreadPoolExecutor会自动创建一个新的线程继续进行工作(Android程序还是会闪退的,Java程序才不会)。
SingleThreadExecutor 适用于在逻辑上需要单线程处理任务的场景,同时无界的LinkedBlockingQueue保证新任务都能够放入队列,不会被拒绝;缺点和FixedThreadPool相同,当处理任务无限等待的时候会造成内存问题。
3、CachedThreadPool - 线程池无限大(MAX INT),等待队列长度为1

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

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、等待获取锁资源。

4.7 线程的生命周期

原文阅读>>


参考资料

ThreadPoolExecutor策略配置以及应用场景