线程池
一、为什么要用线程池?
使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
比较重要的几个类:
类名 | 描述 |
ExecutorService | 真正的线程池接口。 |
ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 |
ThreadPoolExecutor | ExecutorService的默认实现。 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
二、ExecutorService
1、ExecutorService的创建(两种方式)ThreadPoolExecutor、Executors 工具类
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
方式一:通过 ThreadPoolExecutor 构造方法实现
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize, // 线程池维护线程的最少数量,核心线程数:最小可以同时运行线程数量
int maximumPoolSize, // 线程池维护线程的最大线程数:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
long keepAliveTime, // 线程池维护线程所允许的空闲时间
TimeUnit unit, // 线程池维护线程所允许的空闲时间的单位
BlockingQueue<Runnable> workQueue, // 线程池所使用的缓冲队列
ThreadFactory threadFactory, //
RejectedExecutionHandler handler) { // 线程池对拒绝任务的处理策略,饱和策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize
和 maximumPoolSize
线程数主要由corePoolSize
和 maximumPoolSize
控制。
corePoolSize
:线程池维护线程的最少数量
maximumPoolSize
:线程池维护线程的最大线程数
具体线程的分配方式
当一个任务被添加到线程池:
- 如果此时
线程池中的数量
<corePoolSize
,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。 - 如果此时
线程池中的数量
=corePoolSize
,但是缓冲队列workQueue
未满,那么任务被放入缓冲队列。 - 如果此时
线程池中的数量
>corePoolSize
:
- 缓冲队列
workQueue
满,并且线程池中的数量
<maximumPoolSize
,建新的线程来处理被添加的任务。 - 缓冲队列
workQueue
满,并且线程池中的数量
=maximumPoolSize
,那么通过handler
所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。 - 某
线程空闲时间
>keepAliveTime
,线程将被终止
unit
unit 可选的参数为java.util.concurrent.TimeUnit
中的几个静态属性:
- NANOSECONDS
- MICROSECONDS
- MILLISECONDS
- SECONDS
workQueue
workQueue是一个BlockingQueue阻塞队列,默认是LinkedBlockingQueue<Runnable>
handler
handler 是线程池拒绝处理任务的方式,主要有四种类型:
-
ThreadPoolExecutor.AbortPolicy
: 抛出RejectedExecutionException
来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy
: 调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。 -
ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
举个例子:
Spring 通过 ThreadPoolTaskExecutor
或者我们直接通过 ThreadPoolExecutor
的构造函数创建线程池的时候,
当我们不指定 RejectedExecutionHandler
饱和策略的话来配置线程池的时候,默认使用的是 ThreadPoolExecutor.AbortPolicy
。在默认情况下,ThreadPoolExecutor
将抛出 RejectedExecutionException
来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。
对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy
。当最大池被填满时,此策略为我们提供可伸缩队列。
一个Demo
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
// Demo1: 单个
public static void testThreadPoolExecutor() throws ExecutionException, InterruptedException {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
// 1.execute实现Runnable的run接口
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("这是第1个线程哦哦哦哦哦哦哦");
}
});
// 2.execute 方法
executor.execute(() -> {
// do something
});
// 3.submit 方法 返回值,用 Future接收,并future.get()获取值
Future future = executor.submit(() -> {
// do something
return 1;
});
future.get(); // 获取值
// 最后:终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("所有线程已结束");
}
// Demo2变体:List<Future<返回值类型>>
public static void testThreadPoolExecutor2() throws ExecutionException, InterruptedException {
List<Future<Integer>> futureList = new ArrayList<>(); // Integer可改成任何想返回的类
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
futureList.add(executor.submit(() -> {
// do somethings
return 1; // 返回想要的值
}));
executor.shutdown();
while (!executor.isTerminated()) {
try {
executor.awaitTermination(200, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// 记录错误日志
}
}
Iterator<Future<Integer>> iter = futureList.iterator();
while (iter.hasNext()) {
Future<Integer> future = iter.next();
if (future.isDone()) {
future.get(); // 获取值 do something
}
}
}
}
方式二:通过 Executor 框架的工具类 Executors 来实现(不推荐)
Executors工具类的
-
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 -
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 -
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。 -
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
一个Demo
上述 ExecutorService
创建的地方改成:
2、ExecutorService的执行
ExecutorService有如下几个执行方法:
- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(...)
- invokeAll(...)
execute(Runnable)
分析一下 execute
方法
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int workerCountOf(int c) {
return c & CAPACITY;
}
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) {
// 如果任务为null,则抛出异常。
if (command == null)
throw new NullPointerException();
// ctl 中保存的线程池当前的一些状态信息
int c = ctl.get();
// 下面会涉及到 3 步 操作
// 1.首先判断当前线程池中执行的任务数量是否 < corePoolSize
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前执行的任务数量 >= corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态并且队列可以加入任务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
// 如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
具体参考:
具体线程的分配方式
当一个任务被添加到线程池:
- 如果此时
线程池中的数量
<corePoolSize
,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。- 如果此时
线程池中的数量
=corePoolSize
,但是缓冲队列workQueue
未满,那么任务被放入缓冲队列。- 如果此时
线程池中的数量
>corePoolSize
:
- 缓冲队列
workQueue
满,并且线程池中的数量
<maximumPoolSize
,建新的线程来处理被添加的任务。- 缓冲队列
workQueue
满,并且线程池中的数量
=maximumPoolSize
,那么通过handler
所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。- 某
线程空闲时间
>keepAliveTime
,线程将被终止
submit(Runnable) 无返回值(返回一个null)
submit(Runnable)
和execute(Runnable)
区别是前者可以返回一个Future对象,通过返回的Future对象,我们可以检查提交的任务是否执行完毕,请看下面执行的例子:
Future future = executorService.submit(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
});
future.get(); //returns null if the task has finished correctly
如果任务执行完成,future.get()
方法会返回一个null。注意,future.get()方法会产生阻塞。
submit(Callable) 有返回值
submit(Callable)
和 submit(Runnable)
类似,也会返回一个 Future
对象,但是除此之外,submit(Callable)
接收的是一个Callable的实现,Callable接口中的call()方法有一个返回值,可以返回任务的执行结果,而Runnable接口中的run()方法是void的,没有返回值。请看下面实例:
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
});
System.out.println("future.get() = " + future.get());
如果任务执行完成,future.get()方法会返回Callable任务的执行结果。注意,future.get()方法会产生阻塞。
execute()方法和 submit()方法的区别是什么呢?
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;-
submit()
方法用于提交需要返回值的任务。线程池会返回一个Future
类型的对象,通过这个Future
对象可以判断任务是否执行成功,并且可以通过Future
的get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
invokeAny(…Callable集合) 返回所有Callable任务中其中一个任务的执行结果
invokeAny(...)
方法接收的是一个Callable的集合,执行这个方法不会返回Future
,但是会返回所有Callable任务中其中一个任务的执行结果。这个方法也无法保证返回的是哪个任务的执行结果,反正是其中的某一个。请看下面实例:
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
String result = executor.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();
invokeAll(…Callable集合) 返回一个Future
的List
invokeAll(...)
与 invokeAny(...)
类似也是接收一个Callable集合,但是前者执行之后会返回一个Future
的List,其中对应着每个Callable任务执行后的Future对象。情况下面这个实例:
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
});
List<Future<String>> futures = executorService.invokeAll(callables);
for(Future<String> future : futures){
System.out.println("future.get = " + future.get());
}
executorService.shutdown();