不讲概念,来记录下常见线程池的配置以及区别。
先来回顾下常见的配置参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
使用
newFixedThreadPool
该方法返回一个固定大小的线程池,该线程从中的线程数量固定不变,当有任务提交时如果有空闲线程,立即执行,否则加入等待队列,直到有空闲线程时再执行。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
可以看到corePoolSize=maximumPoolSize,并且等待队列大小为Integer.MAX_VALUE,也就是说这是一个固定大小的线程池,可以无限接收任务直到服务器挂了。
示例:
ExecutorService pool = Executors.newFixedThreadPool(3, new ThreadFactory() {
AtomicLong count = new AtomicLong(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("nameThreadFactory-" + count.getAndIncrement());
return thread;
}
});
int i = 0;
for (; i < 10; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " go");
});
}
//必须结束,否则线程无法关闭
pool.shutdown();
console output:
nameThreadFactory-1 go
nameThreadFactory-3 go
nameThreadFactory-2 go
nameThreadFactory-1 go
nameThreadFactory-3 go
nameThreadFactory-2 go
nameThreadFactory-3 go
nameThreadFactory-1 go
nameThreadFactory-3 go
nameThreadFactory-2 go
可以看出来来回回只有三个线程在切换执行。
newCachedThreadPool
该方法返回一个可根据实际情况调整数量的线程池,数量不固定,但优先会复用线程,如果当前线程数量不够,再创建新的线程。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
初始大小为 0,最大大小无上限,也就代表了可以一直创建新的线程,这里的等待队列SynchronousQueue比较特殊,是一个直接提交的队列,不会保存即总是将任务交给线程执行,也就是说newCachedThreadPool是不带缓冲队列的。
示例:
ExecutorService pool = Executors.newCachedThreadPool(new ThreadFactory() {
AtomicLong count = new AtomicLong(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("nameThreadFactory-" + count.getAndIncrement());
return thread;
}
});
int i = 0;
for (; i < 10; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " go");
});
}
//必须结束,否则线程无法关闭
pool.shutdown();
console output:
nameThreadFactory-1 go
nameThreadFactory-2 go
nameThreadFactory-3 go
nameThreadFactory-4 go
nameThreadFactory-5 go
nameThreadFactory-6 go
nameThreadFactory-7 go
nameThreadFactory-8 go
nameThreadFactory-4 go
nameThreadFactory-8 go
可以看出有重复的线程在执行任务,这就是复用的表现。
newScheduledThreadPool
定时调度的线程池在中间件研发中经常会被使用到,其中主要包含三个方法:
//将在delay单位的延迟后开始执行一次任务
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
//以固定的速率执行任务,initialDelay代表第一次执行需要延迟的时间,period代表多久重复一次
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
//以固定的延迟周期执行任务,即每个任务执行相差delay时间
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3, new ThreadFactory() {
AtomicLong count = new AtomicLong(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("nameThreadFactory-" + count.getAndIncrement());
return thread;
}
});
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " time:" + System.currentTimeMillis() + " go");
}
};
Runnable scheduleAtFixedRateTask = new Runnable() {
@Override
public void run() {
System.out.println("scheduleAtFixedRateTask" + " begin time:" + System.currentTimeMillis() + " go");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("scheduleAtFixedRateTask" + " end time:" + System.currentTimeMillis() + " go");
}
};
// 1秒过后执行
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS);
// 1秒以后开始启动,每过两秒执行一次,如果上一个工作超过了2秒,那么下一个并不会开始,而是等到上一个结束后立马开始。
scheduledThreadPool.scheduleAtFixedRate(scheduleAtFixedRateTask, 1, 2, TimeUnit.SECONDS);
// 1秒以后开始启动,每个任务之间间隔2秒,无论上一个任务执行了多久,下一个任务总是会在上一个任务结束后2秒开始。
scheduledThreadPool.scheduleWithFixedDelay(task, 1, 2, TimeUnit.SECONDS);
// 结束后,两个定时执行的都无法工作
// scheduledThreadPool.shutdown();
}
以上程序的输出就不再展示了,简单说下现象:
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS);会在一秒后执行一次结束。
scheduledThreadPool.scheduleAtFixedRate(scheduleAtFixedRateTask, 1, 2, TimeUnit.SECONDS);就比较有意思了,这里设定的是程序开始一秒后开始执行第一次计划任务,每隔 2 秒重复一次。如果任务执行时间小于 2 秒,那自然没什么问题。但是这里设置的任务时长为 3 秒,会发现上一个任务没结束,即使到了 2 秒的间隔,下一个也不会开始,而是痴情的等待上一个任务的结束,只要它一结束,下一个会立刻开始。
scheduleAtFixedRateTask begin time:1555324763580 go
scheduleAtFixedRateTask end time:1555324766581 go
scheduleAtFixedRateTask begin time:1555324766581 go
输出就是如此这般。
scheduledThreadPool.scheduleWithFixedDelay(task, 1, 2, TimeUnit.SECONDS);是比较好理解的,无论上一个任务执行多久,只要它一结束后再过 2 秒,下一个任务即开始,没什么特殊的地方。
拾遗
由于对线程池的实现比较熟悉,所以不再进行详细的记录,简单的记录下要注意的地方:
- 饱和策略
当线程池负载高,无法处理新投递的任务时有几种拒绝策略:
- 抛出异常
- 谁调用谁执行
- 丢弃队列中最老的任务,也就是即将执行的任务
- 直接丢弃当前任务
2)线程数设置多大
一般 IO 密集型 CPU*2,计算密集型 CPU+1
3) 丢失的异常堆栈
如果执行的任务报错了,并且刚好没有捕获任何异常,那么不好意思,堆栈丢了,要么自己捕获Runable,要么扩展线程池,可以看看Guava中对线程池的扩展,或者唯品会的vipTools。
如果有人问线程池是咋回事,你就这么说
曾经有一个创业公司,它就是线程池。开张的时候拉了 4 个兄弟,就是 coreSize=4,后来公司发展的太快,需求越来越多,兄弟们忙不过来了,怎么办只能任务排期了,但排期总有个上限吧,不能压着 10000 个需求不做吧?这个个数呢就是队列的长度。这个时候公司扩招了,技术部最多招 30 个兄弟。好了,兄弟们来了,10000 个需求也搞完了。这个时候呢,9012 年了经济危机了资本家不乐意了,公司不养闲人啊,那个老王,你看你们技术部那些新来的(maxSize-coreSize)有哪些一个月没干活了(aliveTime)开了吧。呵呵。。
纪念下那些被开的兄弟。(刘强东说了不能奋斗的人都不是我的兄弟)