Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
最近项目用频繁用到它,有必要总结下。
首先看它的好处:
以装修公司类比线程池
以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。
线程池就是程序中的“装修公司”,代劳各种脏活累活。
// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 正式工数量
int maximumPoolSize, // 工人数量上限,包括正式工和临时工
long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇
BlockingQueue<Runnable> workQueue, // 排期队列
ThreadFactory threadFactory, // 招人渠道
RejectedExecutionHandler handler) // 拒单方式
1 基础知识:
Executors
创建线程池
Java中创建线程池很简单, 只需要调用 Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),
方便的同时也隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
Execute创建线程池便捷方法列表:
newFixedThreadPool(int nThreads)
创建固定大小的线程池newSingleThreadExecutor()
创建只有一个线程的线程池newCachedThreadPool()
创建一个不限线程数上限的线程池,任何提交的任务都将立即执行。小程序使用这些快捷方法都没什么问题,对于服务器需要长期运行的程序,创建线程池应该直接使用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) // 拒绝策略
这些参数中,比较容易引起问题的有 corePoolSize,maxmumPoolSize, workQueue以及handler:
线程池的工作顺序:
If fewer than corePoolSize threads are running, the Exector 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 case, the task will be rejected.
2.线程池的使用:
2.1 线程池的创建
new ThreadPoolExecutor(corePoolSize,maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue,handler);
2.2 向线程池提交任务
threadPool.execute(new Runnable() {
@override
public void run(){
//TODO auto generated method stub
}
});
execute()
方法用于提供不需要返回值的任务。
submit()
方法用于提交需要返回值的任务。线程池会返回一个future
类型的对象,通过这个future
对象可以判断任务是否执行成功,并且可以通过future
的get()
方法来获取返回值,get()
方法会阻塞当前线程知道任务完成。而使用get(long timeout, TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
Future<Object> future = executor.submit(harReturnValuetask);
try{
Object s = future.get();
}catch(InterruptedException e){
//处理中断异常
}catch(ExecutionException e){
//处理无法执行任务异常
}finally{
//关闭线程池
executor.shutdown():
}
2.3 关闭线程池
可以通过调用线程池的shutdown
或shutdownNow
方法来关闭线程池。
2.4 合理地配置线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
任务的优先级:高、中和低。
任务的执行时间:长、中和短。
性质的依赖性:是否依赖其他系统资源、如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置N+1个线程的线程池。由于IO密集型任务线程并不是一致在执行任务,则应配置尽可能多的线程,如2*N。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()
方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列ProprityBlockingQueue
来处理。它可以让优先级高的任务先执行。
不要使用Executors.newXXXThreadPool()
快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免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
,也就是抛出RejectedExecutionHandler
异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy
,这样多余的任务会悄悄的被忽略。
3. 获取处理结果和异常
线程池的处理结果、以及处理过程中的异常都被包装到Future
中,并在调用Future.get()
方法时获取,执行过程中的异常会被包装成ExecutionException
,submit()
方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:
ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者
}
});
try {
Object result = future.get();
} catch (InterruptedException e) {
// interrupt
} catch (ExecutionException e) {
// exception in Callable.call()
e.printStackTrace();
}
2.5 线程池的监控
如果在系统中大量使用线程池,则有必要对线程池进行监控。快速定位问题,可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性。
beforeExecute
,afterExecute
和termineated
方法,也可以在任务执行前,执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。 protected void beforeExecute(Thread t, Runnable r){}
总结:
Executors
为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor
的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService
提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch
完成。
参考:
1) https://www.cnblogs.com/CarpenterLee/p/9558026.html
2)