构造一个线程池为何须要几个参数?若是避免线程池出现OOM?Runnable
和Callable
的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片断。小程序
Java中建立线程池很简单,只须要调用Executors
中相应的便捷方法便可,好比Executors.newFixedThreadPool(int nThreads)
,可是便捷不只隐藏了复杂性,也为咱们埋下了潜在的隐患(OOM,线程耗尽)。服务器
Executors
建立线程池便捷方法列表:less
方法名 | 功能 |
---|---|
newFixedThreadPool(int nThreads) | 建立固定大小的线程池 |
newSingleThreadExecutor() | 建立只有一个线程的线程池 |
newCachedThreadPool() | 建立一个不限线程数上限的线程池,任何提交的任务都将当即执行 |
小程序使用这些快捷方法没什么问题,对于服务端须要长期运行的程序,建立线程池应该直接使用ThreadPoolExecutor
的构造方法。没错,上述Executors
方法建立的线程池就是ThreadPoolExecutor
。ide
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
:ui
corePoolSize
和maximumPoolSize
设置不当会影响效率,甚至耗尽线程;this
workQueue
设置不当容易致使OOM;spa
handler
设置不当会致使提交任务时抛出异常。线程
正确的参数设置方式会在下文给出。code
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 case, the task will be rejected.
corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略
能够向线程池提交的任务有两种:Runnable
和Callable
,两者的区别以下:
方法签名不一样,void Runnable.run()
, V Callable.call() throws Exception
是否容许有返回值,Callable
容许有返回值
是否容许抛出异常,Callable
容许抛出异常。
Callable
是JDK1.5时加入的接口,做为Runnable
的一种补充,容许有返回值,容许抛出异常。
提交方式 | 是否关心返回结果 |
---|---|
Future<T> submit(Callable<T> task) |
是 |
void execute(Runnable command) |
否 |
Future<?> submit(Runnable task) |
否,虽然返回Future,可是其get()方法老是返回null |
不要使用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 | 抛出RejectedExecutionException |
DiscardPolicy | 什么也不作,直接忽略 |
DiscardOldestPolicy | 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置 |
CallerRunsPolicy | 直接由提交任务者执行这个任务 |
线程池默认的拒绝行为是AbortPolicy
,也就是抛出RejectedExecutionHandler
异常,该异常是非受检异常,很容易忘记捕获。若是不关心任务被拒绝的事件,能够将拒绝策略设置成DiscardPolicy
,这样多余的任务会悄悄的被忽略。
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(512), new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略
线程池的处理结果、以及处理过程当中的异常都被包装到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(); }
上述代码输出相似以下:
int poolSize = Runtime.getRuntime().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 Future.get()
方法可以阻塞等待执行结果,V get(long timeout, TimeUnit unit)
方法能够指定等待的超时时间。
若是向线程池提交了多个任务,要获取这些任务的执行结果,能够依次调用Future.get()
得到。但对于这种场景,咱们更应该使用ExecutorCompletionService,该类的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) use(r); } }
V Future.get(long timeout, TimeUnit unit)
方法能够指定等待的超时时间,超时未完成会抛出TimeoutException
。
等待多个任务完成,并设置最大等待时间,能够经过CountDownLatch完成:
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.countDown();// countDown } } }); } 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
完成。