文章目录
一、常见线程池
并发编程离不开线程的使用,线程离不开线程池的使用。这里简单总结下ThreadPoolExecutor的参数及场景。
Executors 是 JUC提供的线程池使用工具类,里面定义了四种线程池的生成方法,我们从这里入手进行解释。
1. 只有一个线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
singleThreadExecutor是只有一个线程的线程池,十分干脆没啥说的,下图是newSingleThreadExecutor方法内部实现,ThreadPoolExecutor构造参数里面的第一个corePoolSize和maximumPoolSize都是1,这两个参数分别代表核心线程数和最大线程数。
2. 固定数量线程的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
fixedThreadPool 是固定线程数量的线程池,核心线程数、最大线程数都是这个固定数量,除此之外与singleThreadExecutor 完全一样。
3. 可以缓存空闲线程的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
cachedThreadPool 是可以缓存空闲线程的线程池,怎么缓存呢? ** ThreadPoolExecutor构造方法提供了一个参数叫做 keepAliveTime,这个参数表示非核心线程空闲后的存活时间,一旦空闲超过这个时间该线程就会被销毁。 **
可以回头去看上面两种线程池这个参数都是0L,表示非核心的线程在执行完任务后会直接销毁,而cachedThreadPool默认会保留60s,这就是缓存线程的原理。
4. 可以延时/定时的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
// 10s后再执行该任务
scheduledExecutorService.schedule(()-> System.out.println("执行任务"),10,TimeUnit.SECONDS);
// 第一次是0s后执行,之后每隔10s执行一次
scheduledExecutorService.scheduleAtFixedRate(()-> System.out.println("执行任务"),0,10,TimeUnit.SECONDS);
scheduledExecutorService 与 之前三种最大的不同点在于,之前三种是通过new ThreadPoolExecutor得到的,而定时类线程池是通过new ScheduledThreadPoolExecutor得到的,ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类,封装了定时的特性。
延时又是如何实现的呢?ThreadPoolExecutor有一个参数叫做workQueue,我们提交一个任务到线程池后,线程池会先将任务放入到这个队列中,然后线程池再按照规则指定一个线程来执行这个任务。这里的workQueue可以设置为DelayedWorkQueue,以此实现任务的延时执行。
二、ThreadPoolExecutor
通过上面几种线程池的介绍,我们知道这些线程池都是从ThreadPoolExecutor通过一些构造参数的设置得到的,下面就来看一看这个神奇的构造函数。
/**
* 核心线程数、最大线程数、非核心线程数多久被销毁、keepAliveTime的单位、队列(一般为sync、有界、无界三种)、拒绝策略(默认为AbortPolicy,有长任务时可以考虑DiscardOldestPolicy)
*/
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(1,10,60L
, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(1),new ThreadPoolExecutor.AbortPolicy());
- corePoolSize : 核心线程数。常备线程,这些线程空闲 也不会丢弃。
- maxiumPoolSize : 最大线程数。代表线程最多能开多少个,若是超过这个数值,新来的任务就需要执行拒绝策略。
- keepAliveTime : 空闲线程存活时间。如前面所说,非核心线程再工作完后一般是直接销毁,如果想让其保留一段时间,可以设置该参数。
- workQueue : 工作队列,常用的有sync、ArrayBlocking、LinkedBlocking,分别代表无队列、有界队列、无界队列。也可以自己实现该队列,DelayQueue就是另外一种特殊的队列实现,只有队列中的元素过期了才能出队列。
- handler : 拒绝策略。
- AbortPolicy : 丢弃新来的任务并抛出RejectedExecutionException异常。
- DiscardPolicy : 丢弃新来的任务,但是不抛出异常。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。
ThreadPoolExecutor的核心参数大致如上所述,但是如何组合去使用他们,在什么样的场景下使用还是个问题。
三、使用场景。
- 需要持续的处理大量、执行时间段的任务,如何设置线程池的参数?
线程数设置为与cpu核心接近的数量,减少频繁的线程上下文切换。
增长keepAliveTime或将核心线程数和最大线程数设置一样,减少频繁的线程创建。 - 需要处理少量、执行时间长的任务,如何设置?
如果任务为IO密集型,cpu压力不大,可以适当加大线程数量。
如果任务为计算密集型,cpu本身就压力很大,那么还是将线程与核心数相近,减少线程切换。
同时需要考虑将拒绝策略设置为 DiscardOldestPolicy ,防止有任务太长时间阻塞。 - 任务绝大多数都是短的,少部分是长任务,如何设置?
线程数同1,主要还是考虑拒绝策略,不能让这些少量的长任务阻塞整个线程池。
四、自定义线程池demo
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 核心线程数、最大线程数、非核心线程数多久被销毁、keepAliveTime的单位、队列(一般为sync、有界、无界三种)、拒绝策略(默认为AbortPolicy,有长任务时可以考虑DiscardOldestPolicy)
* ForkJoinPool 不同于其他线程池。
*/
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(1,10,60L
, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(1),new ThreadPoolExecutor.DiscardPolicy().AbortPolicy());
// submit可以传入callable,返回future。
Future<Integer> future = poolExecutor.submit(() -> {
Thread.sleep(1000);
return 1;
});
while( !future.isDone() ){
System.out.println("等待计算线程给我数据");
}
System.out.println( future.get() );
if( !poolExecutor.isShutdown() ){
poolExecutor.shutdown();
}
}