并发编程,从工作以来就很好奇,学习整理过程中老是断断续续的,工作中也没能用上,也就一直没能整理,这次利用时间好好的整理下,结合网上的文章,如有相同,实属借鉴。
1 线程池
1.1 定义
对象池技术。某些对象(比如线程,数据库连接等),它们创建的代价是非常大的,相比于一般对象,它们创建消耗的时间和内存都很大(而且这些对象销毁的代价比一般对象也大)。同时,这些对象用完之后必须销毁,否则会占据大量的资源而导致服务器崩溃。所以,如果我们维护一个池,每次使用完这些对象之后,并不销毁它,而是将其放入池中,下次需要使用时就直接从池中取出,便可以避免这些对象的重复创建;同时,我们可以固定池的大小,比如设置池的大小为 N(即池中只保留 N 个这类对象),当池中的N个对象都在使用中的时候,为超出数量的请求设置一种策略,比如 排队等候 或者 直接拒绝请求 等,从而避免频繁的创建此类对象。
线程池。即对象池的一种(池中的对象为线程 Thread),类似的还有数据库连接池(池中对象为数据库连接 Connection)。合理利用线程池能够带来三个好处(参考文末的 References[1]):
- 降低资源消耗,通过重复利用已创建的线程,降低线程创建和销毁时造成的时间和内存上的消耗;
- 提升响应速度,当任务到达时,直接使用线程池中的线程来运行任务,使得任务可以不需要等到线程创建就能立即执行;
- 提高线程的可管理性,线程是开销很大的对象,如果无限制的创建线程,不仅会快速消耗系统资源,还会降低系统的稳定性;可以对线程进行统一的分配和调控。
- 可以根据系统的承受能力,调整线程池中工作线程的数量,防止服务器过载,形成内存溢出,或者CPU耗尽。
1.2 创建线程池步骤和实例
步骤:
1)创建一个线程池对象,控制要创建几个线程对象。
public static ExecutorService newFixedThreadPool(int nThreads)
2)这种线程池的线程可以执行:
可以执行Runnable对象或者Callable对象代表的线程
做一个类实现Runnable接口。
3)调用如下方法即可
Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
4)结束线程池
实例:接下来感受一下线程池的魅力。
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 10; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
public class ExecutorServiceDemo {
public static void main(String[] args) {
// 创建一个线程池对象,控制要创建几个线程对象。
// public static ExecutorService newFixedThreadPool(int nThreads)
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
// 结束线程池
pool.shutdown();
}
}
结果:
pool-1-thread-2:0
pool-1-thread-2:1
pool-1-thread-2:2
pool-1-thread-2:3
pool-1-thread-2:4
pool-1-thread-2:5
pool-1-thread-2:6
pool-1-thread-2:7
pool-1-thread-1:0
pool-1-thread-1:1
pool-1-thread-1:2
pool-1-thread-1:3
pool-1-thread-1:4
pool-1-thread-1:5
pool-1-thread-1:6
pool-1-thread-1:7
pool-1-thread-1:8
pool-1-thread-1:9
pool-1-thread-2:8
pool-1-thread-2:9
1.3 JDK线程池继承架构
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。
真正的线程池接口是ExecutorService。下面这张图完整描述了线程池的类体系结构。
- Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;
- ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
- 抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
- ThreadPoolExecutor继承了类AbstractExecutorService。
比较重要的类:
ExecutorService | 真正的线程池接口。 |
ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 |
ThreadPoolExecutor | ExecutorService的默认实现。 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
Executors类里面提供了一些静态工厂,生成一些常用的线程池。
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。
- 如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
- 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,
- 当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
- 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
- 这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。
它提供了如下方法来提交一个任务:
- Future<?> submit(Runnable task)
- <T> Future<T> submit(Callable<T> task)
Callable 与 Runable的相关内容参见:
1.4 线程池的处理流程
重要:线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果阻塞队列满了,那就创建新的线程执行当前任务;直到线程池中的线程数达到maxPoolSize,这时再有任务来,只能执行reject()处理该任务;
创建线程池需要使用 ThreadPoolExecutor
类,它的构造函数参数如下:
public ThreadPoolExecutor(int corePoolSize, //核心线程的数量
int maximumPoolSize, //最大线程数量
long keepAliveTime, //超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间的单位
BlockingQueue<Runnable> workQueue, //保存待执行任务的队列
ThreadFactory threadFactory, //创建新线程使用的工厂
RejectedExecutionHandler handler // 当任务无法执行时的处理器
) {...}
参数介绍如注释所示,要了解这些参数做什么的,就需要了解线程池具体的执行方法ThreadPoolExecutor.execute
:
public void execute(Runnable command) {
if (command == null) {
throw new NullPointerException();
}
int c = ctl.get();
// 1.当前池中线程比核心数少,新建一个线程执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.核心池已满,但任务队列未满,添加到队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command)) // 如果这时被关闭了,拒绝任务
reject(command);
else if (workerCountOf(recheck) == 0) // 如果之前的线程已被销毁完,新建一个线程
addWorker(null, false);
}
// 3.核心池已满,队列已满,试着创建一个新线程
else if (!addWorker(command, false)) {
reject(command); // 如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
}
}
可以看到,线程池处理一个任务主要分三步处理,代码注释里已经介绍了,我再用通俗易懂的例子解释一下:
线程比作员工,线程池比作一个团队,核心池比作团队中核心团队员工数,核心池外的比作外包员工
有了新需求,先看核心员工数量超没超出最大核心员工数,还有名额的话就新招一个核心员工来做(需要获取全局锁)
核心员工已经最多了,且HR不给批HC了,那这个需求只好攒着,放到待完成任务列表吧
如果列表已经堆满了,核心员工基本没机会搞完这么多任务了,那就找个外包吧(需要获取全局锁)
如果核心员工 + 外包员工的数量已经是团队最多能承受人数了,没办法,这个需求接不了了
结合这张图,这回流程你明白了吗?
由于 1 和 3 新建线程时需要获取全局锁,这将严重影响性能。因此ThreadPoolExecutor 这样的处理流程是为了在执行 execute() 方法时尽量少地执行 1 和 3,多执行 2。
备注:在 ThreadPoolExecutor 完成预热后(当前线程数不少于核心线程数),几乎所有的 execute() 都是在执行步骤 2。
前面提到的 ThreadPoolExecutor 构造函数的参数,内容如下:
- corePoolSize:核心线程池数量
- 在线程数少于核心数量时,有新任务进来就新建一个线程,即使有的线程没事干
- 等超出核心数量后,就不会新建线程了,空闲的线程就得去任务队列里取任务执行了
- maximumPoolSize:最大线程数量
- 包括核心线程池数量 + 核心以外的数量
- 如果任务队列满了,并且池中线程数小于最大线程数,会再创建新的线程执行任务
- keepAliveTime:核心池以外的线程存活时间,即没有任务的外包的存活时间
- 如果给线程池设置 allowCoreThreadTimeOut(true),则核心线程在空闲时头上也会响起死亡的倒计时
- 如果任务是多且容易执行的,可以调大这个参数,那样线程就可以在存活的时间里有更大可能接受新任务
- workQueue:保存待执行任务的阻塞队列
- 不同的任务类型有不同的选择,下一小节介绍
- threadFactory:每个线程创建的地方
- 可以给线程起个好听的名字,设置个优先级啥的
- handler:饱和策略,大家都很忙,咋办呢,有四种策略
- CallerRunsPolicy:只要线程池没关闭,就直接用调用者所在线程来运行任务
- AbortPolicy:直接抛出 RejectedExecutionException 异常
- DiscardPolicy:悄悄把任务放生,不做了
- DiscardOldestPolicy:把队列里待最久的那个任务扔了,然后再调用 execute() 试试看能行不
- 也可以实现自己的 RejectedExecutionHandler 接口自定义策略,比如如记录日志什么的
1.5 保存待执行任务的阻塞队列
当线程池中的核心线程数已满时,任务就要保存到队列中了。
线程池中使用的队列是 BlockingQueue
接口,常用的实现有如下几种:
- ArrayBlockingQueue:基于数组、有界,按 FIFO(先进先出)原则对元素进行排序
- LinkedBlockingQueue:基于链表,按FIFO (先进先出) 排序元素
- 吞吐量通常要高于 ArrayBlockingQueue
- Executors.newFixedThreadPool() 使用了这个队列
- SynchronousQueue:不存储元素的阻塞队列
- 每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
- 吞吐量通常要高于 LinkedBlockingQueue
- Executors.newCachedThreadPool使用了这个队列
- PriorityBlockingQueue:具有优先级的、无限阻塞队列
关于阻塞队列的详细介绍请看这 2 篇:
1.6 创建自己的线程池
1. 先定义线程池的几个关键属性的值:
private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心线程数为 CPU 数*2
private static final int MAXIMUM_POOL_SIZE = 64; // 线程池最大线程数
private static final int KEEP_ALIVE_TIME = 1; // 保持存活时间 1秒
- 设置核心池的数量为 CPU 数的两倍,一般是 4、8,好点的 16 个线程
- 最大线程数设置为 64
- 空闲线程的存活时间设置为 1 秒
2. 根据处理的任务类型选择不同的阻塞队列
如果是要求高吞吐量的,可以使用 SynchronousQueue 队列;如果对执行顺序有要求,可以使用 PriorityBlockingQueue;如果最大积攒的待做任务有上限,可以使用 LinkedBlockingQueue。
private final BlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>(128);
3. 创建自己的 ThreadFactory
在其中为每个线程设置个名称:
private final ThreadFactory DEFAULT_THREAD_FACTORY
=
new ThreadFactory() {
private final AtomicInteger mCount
=
new AtomicInteger(
1);
public
Thread newThread(Runnable r) {
Thread
thread
=
new
Thread(r,
TAG
+
" #"
+ mCount
.getAndIncrement());
thread.setPriority(
Thread.NORM_PRIORITY);
return
thread;
}
};
4. 然后就可以创建线程池了
private ThreadPoolExecutor mExecutor =
new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,
TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,
new ThreadPoolExecutor.DiscardOldestPolicy());
这里我们选择的饱和策略为 DiscardOldestPolicy
,你可以可以创建自己的。
5. 完整代码
public class ThreadPoolManager {
private final String TAG = this.getClass().getSimpleName();
private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心线程数为 CPU数*2
private static final int MAXIMUM_POOL_SIZE = 64; // 线程队列最大线程数
private static final int KEEP_ALIVE_TIME = 1; // 保持存活时间 1秒
// 使用 LinkedBlockingQueue作为阻塞队列
private final BlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>(128);
// 创建ThreadFactory
private final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, TAG + " #" + mCount.getAndIncrement());
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
};
// 创建线程池
private ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,
TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,
new ThreadPoolExecutor.DiscardOldestPolicy()); //选择饱和策略 DiscardOldestPolicy
private static volatile ThreadPoolManager mInstance = new ThreadPoolManager();
public static ThreadPoolManager getInstance() {
return mInstance;
}
public void addTask(Runnable runnable) {
mExecutor.execute(runnable);
}
@Deprecated
public void shutdownNow() {
mExecutor.shutdownNow();
}
}
1.7 JDK 提供的线程池及使用场景
JDK 为我们内置了五种常见线程池的实现,均可以使用 Executors
工厂类创建。
23.7.1 newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
public
static ExecutorService
newFixedThreadPool(
int nThreads) {
return
new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以看到,FixedThreadPool
的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。
此外 keepAliveTime
为 0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义了)。
而这里选用的阻塞队列是 LinkedBlockingQueue
,使用的是默认容量 Integer.MAX_VALUE
,相当于没有上限。
因此这个线程池执行任务的流程如下:
- 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
- 线程数等于核心线程数后,将任务加入阻塞队列
- 由于队列容量非常大,可以一直加加加
- 执行完任务的线程反复去队列中取任务执行
FixedThreadPool
用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
23.7.2 newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。
public
static ExecutorService
newSingleThreadExecutor() {
return
new FinalizableDelegatedExecutorService
(
new ThreadPoolExecutor(
1,
1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
从参数可以看出来,SingleThreadExecutor
相当于特殊的 FixedThreadPool
,它的执行流程如下:
- 线程池中没有线程时,新建一个线程执行任务
- 有一个线程以后,将任务加入阻塞队列,不停加加加
- 唯一的这一个线程不停地去队列里取任务执行
听起来很可怜的样子 - -。
SingleThreadExecutor
用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
23.7.3 newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程
public
static ExecutorService
newCachedThreadPool() {
return
new ThreadPoolExecutor(
0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到,CachedThreadPool
没有核心线程,非核心线程数无上限,也就是全部使用外包,但是每个外包空闲的时间只有 60 秒,超过后就会被回收。
CachedThreadPool
使用的队列是 SynchronousQueue
,这个队列的作用就是传递任务,并不会保存。
因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。
它的执行流程如下:
1. 没有核心线程,直接向 SynchronousQueue
中提交任务
2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜
由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool
不会占用任何资源。
CachedThreadPool
用于并发执行大量短期的小任务,或者是负载较轻的服务器。
23.7.4 newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
public
static ScheduledExecutorService
newScheduledThreadPool(
int corePoolSize) {
return
new ScheduledThreadPoolExecutor(corePoolSize);
}
public
ScheduledThreadPoolExecutor(
int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
private
static
final
long DEFAULT_KEEPALIVE_MILLIS =
10L;
ScheduledThreadPoolExecutor
继承自 ThreadPoolExecutor
, 最多线程数为 Integer.MAX_VALUE
,使用 DelayedWorkQueue
作为任务队列。
ScheduledThreadPoolExecutor
添加任务和执行任务的机制与ThreadPoolExecutor
有所不同。
ScheduledThreadPoolExecutor
添加任务提供了另外两个方法:
scheduleAtFixedRate()
:按某种速率周期执行scheduleWithFixedDelay()
:在某个延迟后执行
它俩的代码如下:
public ScheduledFuture<?>
scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command ==
null || unit ==
null)
throw
new NullPointerException();
if (period <=
0L)
throw
new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period),
sequencer.getAndIncrement());
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
public ScheduledFuture<?>
scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command ==
null || unit ==
null)
throw
new NullPointerException();
if (delay <=
0L)
throw
new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
-unit.toNanos(delay),
sequencer.getAndIncrement());
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
可以看到,这两种方法都是创建了一个 ScheduledFutureTask
对象,调用 decorateTask()
方法转成 RunnableScheduledFuture
对象,然后添加到队列中。
看下 ScheduledFutureTask
的主要属性:
private
class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V> {
//添加到队列中的顺序
private
final
long sequenceNumber;
//何时执行这个任务
private volatile
long time;
//执行的间隔周期
private
final
long period;
//实际被添加到队列中的 task
RunnableScheduledFuture<V> outerTask =
this;
//在 delay queue 中的索引,便于取消时快速查找
int heapIndex;
//...
}
DelayQueue
中封装了一个优先级队列,这个队列会对队列中的 ScheduledFutureTask
进行排序,两个任务的执行 time 不同时,time 小的先执行;否则比较添加到队列中的顺序 sequenceNumber ,先提交的先执行。
ScheduledThreadPoolExecutor
的执行流程如下:
1. 调用上面两个方法添加一个任务
2. 线程池中的线程从 DelayQueue 中取任务
3. 然后执行任务
具体执行任务的步骤也比较复杂:
1. 线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask
DelayQueue.take()
2. 执行完后修改这个 task 的 time 为下次被执行的时间
3. 然后再把这个 task 放回队列中
DelayQueue.add()
ScheduledThreadPoolExecutor
用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
1.8 两种提交任务的方法
ExecutorService
提供了两种提交任务的方法:
execute()
:提交不需要返回值的任务submit()
:提交需要返回值的任务
23.8.1 execute
void execute(Runnable
command);
execute()
的参数是一个 Runnable,也没有返回值。因此提交后无法判断该任务是否被线程池执行成功。
ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(
new Runnable() {
@Override
public
void
run() {
//do something
}
});
23.8.2 submit
<T> Future
<T> submit(Callable
<T> task);
<T> Future
<T> submit(Runnable task, T result);
Future
<?> submit(Runnable task);
同时它会返回一个 Funture
对象,通过它我们可以判断任务是否执行成功。
获得执行结果调用 Future.get()
方法,这个方法会阻塞当前线程直到任务完成。
提交一个 Callable
任务时,需要使用 FutureTask
包一层:
FutureTask futureTask =
new FutureTask(
new Callable<String>() {
//创建 Callable 任务
@Override
public String
call()
throws Exception {
String result =
"";
//do something
return result;
}
});
Future<?> submit = executor.submit(futureTask);
//提交到线程池
try {
Object result = submit.get();
//获取结果
}
catch (InterruptedException e) {
e.printStackTrace();
}
catch (ExecutionException e) {
e.printStackTrace();
}
1.9 关闭线程池
线程池即使不执行任务也会占用一些资源,所以在我们要退出任务时最好关闭线程池。
有两个方法关闭线程池:
23.9.1 shutdown()
public
void
shutdown() {
final ReentrantLock mainLock =
this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//获取权限
advanceRunState(SHUTDOWN);
//修改运行状态
interruptIdleWorkers();
//遍历停止未开启的线程
onShutdown();
// 目前空实现
}
finally {
mainLock.unlock();
}
tryTerminate();
}
private
void
interruptIdleWorkers(
boolean onlyOne) {
final ReentrantLock mainLock =
this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
//遍历所有线程
Thread t = w.thread;
//多了一个条件w.tryLock(),表示拿到锁后就中断
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
}
catch (SecurityException ignore) {
}
finally {
w.unlock();
}
}
if (onlyOne)
break;
}
}
finally {
mainLock.unlock();
}
}
将线程池的状态设置为 SHUTDOWN
,然后中断所有 没有执行 的线程,无法再添加线程。
23.9.2 shutdownNow()
public List<Runnable>
shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock =
this.mainLock;
mainLock.
lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
//修改运行状态
interruptWorkers();
//中断所有
tasks = drainQueue();
}
finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
private
void
interruptWorkers() {
final ReentrantLock mainLock =
this.mainLock;
mainLock.
lock();
try {
for (Worker w : workers)
//中断全部线程,不管是否在执行
w.interruptIfStarted();
}
finally {
mainLock.unlock();
}
}
将线程池设置为 STOP
,然后尝试停止 所有线程,并返回等待执行任务的列表。
不同点是:shutdown()
只结束未执行的任务;shutdownNow()
结束全部。
共同点是:都是通过遍历线程池中的线程,逐个调用 Thread.interrup()
来中断线程,所以一些无法响应中断的任务可能永远无法停止(比如 Runnable
)。
1.10 总结
了解 JDK 提供的几种线程池实现,在实际开发中如何选择呢?
根据任务类型决定。
前面已经介绍了,这里再小结一下:
CachedThreadPool
用于并发执行大量短期的小任务,或者是负载较轻的服务器。FixedThreadPool
用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。SingleThreadExecutor
用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。ScheduledThreadPoolExecutor
用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
队列类型:
ArrayBlockingQueue
:
基于数组、有界,按 FIFO(先进先出)原则对元素进行排序LinkedBlockingQueue
:
基于链表,按FIFO (先进先出) 排序元素SynchronousQueue
:
不存储元素的阻塞队列PriorityBlockingQueue
:
具有优先级的、无限阻塞队列
饱和(拒绝)策略
CallerRunsPolicy
:
只要线程池没关闭,就直接用调用者所在线程来运行任务AbortPolicy
:
直接抛出 RejectedExecutionException 异常DiscardPolicy
:
悄悄把任务放生,不做了DiscardOldestPolicy
:
把队列里待最久的那个任务扔了,然后再调用 execute() 试试看能行不
也可以实现自己的 RejectedExecutionHandler 接口自定义策略,比如如记录日志什么的
自定义线程池时,如果任务是 CPU 密集型(需要进行大量计算、处理),则应该配置尽量少的线程,比如 CPU 个数 + 1,这样可以避免出现每个线程都需要使用很长时间但是有太多线程争抢资源的情况;
如果任务是 IO密集型(主要时间都在 I/O,CPU 空闲时间比较多),则应该配置多一些线程,比如 CPU 数的两倍,这样可以更高地压榨 CPU。
为了错误避免创建过多线程导致系统奔溃,建议使用有界队列。因为它在无法添加更多任务时会拒绝任务,这样可以提前预警,避免影响整个系统。
执行时间、顺序有要求的话可以选择优先级队列,同时也要保证低优先级的任务有机会被执行。