java线程池详解及五种线程池⽅法详解
基础知识
Executors创建线程池
Java中创建线程池很简单,只需要调⽤Executors中相应的便捷⽅法即可,⽐如wFixedThreadPool(int nThreads),但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
Executors创建线程池便捷⽅法列表:
⽅法名功能
newFixedThreadPool(int nThreads)创建固定⼤⼩的线程池
newSingleThreadExecutor()创建只有⼀个线程的线程池
newCachedThreadPool()创建⼀个不限线程数上限的线程池,任何提交的任务都将⽴即执⾏
⼩程序使⽤这些快捷⽅法没什么问题,对于服务端需要长期运⾏的程序,创建线程池应该直接使⽤Threa
dPoolExecutor的构造⽅法。没错,上述Executors⽅法创建的线程池就是ThreadPoolExecutor。
ThreadPoolExecutor构造⽅法
Executors中创建线程池的快捷⽅法,实际上是调⽤了ThreadPoolExecutor的构造⽅法(定时任务使⽤的
是ScheduledThreadPoolExecutor),该类构造⽅法参数列表如下:
// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
int maximumPoolSize, // 线程数的上限
long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
// 超过这个时间,多余的线程会被回收。
BlockingQueue<Runnable> workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产⽣⽅式
RejectedExecutionHandler handler) // 拒绝策略
竟然有7个参数,很⽆奈,构造⼀个线程池确实需要这么多参数。这些参数中,⽐较容易引起问题的
有corePoolSize, maximumPoolSize, workQueue以及handler:
corePoolSize和maximumPoolSize设置不当会影响效率,甚⾄耗尽线程;
workQueue设置不当容易导致OOM;蔬菜做法
handler设置不当会导致提交任务时抛出异常。
正确的参数设置⽅式会在下⽂给出。
线程池的⼯作顺序
If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which ca, the task will be rejected.
corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略
Runnable和Callable
可以向线程池提交的任务有两种:Runnable和Callable,⼆者的区别如下:
豫南会战
1. ⽅法签名不同,void Runnable.run(), V Callable.call() throws Exception
2. 是否允许有返回值,Callable允许有返回值
3. 是否允许抛出异常,Callable允许抛出异常。
Callable是JDK1.5时加⼊的接⼝,作为Runnable的⼀种补充,允许有返回值,允许抛出异常。
蛋糕图案
三种提交任务的⽅式:
提交⽅式是否关⼼返回结果
Future<T> submit(Callable<T> task)是
void execute(Runnable command)否
Future<?> submit(Runnable task)否,虽然返回Future,但是其get()⽅法总是返回null
如何正确使⽤线程池
避免使⽤⽆界队列
不要使⽤wXXXThreadPool()快捷⽅法创建线程池,因为这种⽅式会使⽤⽆界的任务队列,为避免OOM,我们应该使
⽤ThreadPoolExecutor的构造⽅法⼿动指定队列的最⼤长度:
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512), // 使⽤有界队列,避免OOM
new ThreadPoolExecutor.DiscardPolicy());
明确拒绝任务时的⾏为
任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接⼝为我们提供了控制⽅式,接⼝定义如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
线程池给我们提供了⼏种常见的拒绝策略:
拒绝策略拒绝⾏为
AbortPolicy抛出RejectedExecutionException
DiscardPolicy什么也不做,直接忽略
DiscardOldestPolicy丢弃执⾏队列中最⽼的任务,尝试为当前提交的任务腾出位置
CallerRunsPolicy直接由提交任务者执⾏这个任务
线程池默认的拒绝⾏为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是⾮受检异常,很容易忘记捕获。如果不关⼼任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512),
new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略
获取处理结果和异常
线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调⽤()⽅法时获取,执⾏过程中的异常会被包装
成ExecutionException,submit()⽅法本⾝不会传递结果和任务执⾏过程中的异常。获取执⾏结果的代码可以这样写:
ExecutorService executorService = wFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
throw new RuntimeException("exception in call~");// 该异常会在调⽤()时传递给调⽤者
}
});
try {
Object result = ();
} catch (InterruptedException e) {
// interrupt
} catch (ExecutionException e) {
减肥午餐// exception in Callable.call()
e.printStackTrace();
}
上述代码输出类似如下:
线程池的常⽤场景
正确构造线程池
int poolSize = Runtime().availableProcessors() * 2;
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(512);
RejectedExecutionHandler policy = new ThreadPoolExecutor.DiscardPolicy();
executorService = new ThreadPoolExecutor(poolSize, poolSize,
0, TimeUnit.SECONDS,
queue,
policy);
获取单个结果
过submit()向线程池提交任务后会返回⼀个Future,调⽤()⽅法能够阻塞等待执⾏结果,V get(long timeout, TimeUnit unit)⽅法可以指定等待的超时时间。
获取多个结果
如果向线程池提交了多个任务,要获取这些任务的执⾏结果,可以依次调⽤()获得。但对于这种场景,我们更应该使⽤,该类
的take()⽅法总是阻塞等待某⼀个任务完成,然后返回该任务的Future对象。向CompletionService批量提交任务后,只需调⽤相同次数
的CompletionService.take()⽅法,就能获取所有任务的执⾏结果,获取顺序是任意的,取决于任务的完成顺序:
void solve(Executor executor, Collection<Callable<Result>> solvers)
throws InterruptedException, ExecutionException {
CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器
for (Callable<Result> s : solvers)// 提交所有任务
ecs.submit(s);
int n = solvers.size();
for (int i = 0; i < n; ++i) {// 获取每⼀个完成的任务
Result r = ecs.take().get();
if (r != null)
u(r);
}
}
单个任务的超时时间
(long timeout, TimeUnit unit)⽅法可以指定等待的超时时间,超时未完成会抛出TimeoutException。
多个任务的超时时间
等待多个任务完成,并设置最⼤等待时间,可以通过完成:
public void testLatch(ExecutorService executorService, List<Runnable> tasks)原谅我再见都没说
throws InterruptedException{
廉政党课讲稿CountDownLatch latch = new CountDownLatch(tasks.size());
形容樱花
for(Runnable r : tasks){
executorService.submit(new Runnable() {
@Override
public void run() {
try{
r.run();
}finally {
}芥兰怎么做好吃
}
});
}
latch.await(10, TimeUnit.SECONDS); // 指定超时时间
}
线程池和装修公司
以运营⼀家装修公司做个⽐喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式⼯以维持运转;旺季业务较多时,新来的客户请求会被排期,⽐如接单后告诉⽤户⼀个⽉后才能开始装修;当排期太多时,为避免⽤户等太久,公司会通过某些渠道(⽐如⼈才市场、熟⼈介绍等)雇佣⼀些临时⼯(注意,招聘临时⼯是在排期排满之后);如果临时⼯也忙不过来,公司将决定不再接收新的客户,直接拒单。
线程池就是程序中的“装修公司”,代劳各种脏活累活。上⾯的过程对应到线程池上:
// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 正式⼯数量
int maximumPoolSize, // ⼯⼈数量上限,包括正式⼯和临时⼯
long keepAliveTime, TimeUnit unit, // 临时⼯游⼿好闲的最长时间,超过这个时间将被解雇
BlockingQueue<Runnable> workQueue, // 排期队列
ThreadFactory threadFactory, // 招⼈渠道
RejectedExecutionHandler handler) // 拒单⽅式
总结
Executors为我们提供了构造线程池的便捷⽅法,对于服务器程序我们应该杜绝使⽤这些便捷⽅法,⽽是直接使⽤线程
池ThreadPoolExecutor的构造⽅法,避免⽆界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问
题。ExecutorCompletionService提供了等待所有任务执⾏结束的有效⽅式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。
Java 五种线程池详解
在应⽤开发中,通常有这样的需求,就是并发下载⽂件操作,⽐如百度⽹盘下载⽂件、腾讯视频下载视频等,都可以同时下载好⼏个⽂件,这就是并发下载。并发下载处理肯定是多线程操作,⽽⼤量的创建线程,势必会影响程序的性能,导致卡顿等问题。所以呢,Java 中给我们提供了线程池来管理线程。
⾸先,我们来看看线程池是什么?顾名思义,好⽐⼀个存放线程的池⼦,我们可以联想⽔池。线程池意味着可以储存线程,并让池内的线程得以复⽤,如果池内的某⼀个线程执⾏完了,并不会直接摧毁,它有⽣命,可以存活⼀些时间,待到下⼀个任务来时,它会复⽤这个在等待中线程,避免了再去创建线程的额外开销。
百度对线程池的简介:
【线程池(英语:thread pool):⼀种使⽤模式。线程过多会带来调度开销,进⽽影响缓存局部性和整体性能。⽽线程池维护着多个线程,等待着监督管理者分配可并发执⾏的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利⽤,还能防⽌过分调度。可⽤线程数量应该取决于可⽤的并发处理器、处理器内核、内存、⽹络sockets等的数量。 例如,线程数⼀般取cpu数量+2⽐较合适,线程数过多会导致额外的线程切换开销。】
线程池的概念与作⽤就介绍完了,下⾯就是线程池的运⽤了,我们来看这样的⼀个例⼦,模拟⽹络下载的功能,开启多任务下载操作,其中每条下载都开辟新线程来执⾏。
效果图: