线程池相关知识
一、概述
首先思考下线程是否开越多越好呢?答案当然不是的,第一,线程作为对象也是占用资源的,创建和销毁都是需要时间的;第二,线程过多操作系统要频繁切换线程上下文,影响性能。因此就需要线程池来管理线程,调度线程去执行任务。
二、线程池相关类介绍
类型 | 名称 | 说明 |
---|---|---|
接口 | Executor | 顶层接口,定义了执行任务的execute方法 |
接口 | ExecutorService | 继承Executor接口,扩展了shutdown、submit等方法 |
接口 | ScheduledExecutorService | 继承了ExecutorService接口,增加了定时任务相关方法 |
实现类 | ThreadPoolExecutor | 标准的线程池实现 |
实现类 | ScheduledThreadPoolExecutor | 继承了ThreadPoolExecutor,实现了ScheduledExecutorService中定时方法 |
工具类 | Executors | 快速创建指定类型线程池,屏蔽了底层代码的复杂性 |
三、通过Executors源码了解不同线程池的特征
1、Executors.newFixedThreadPool
查看源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
我们可以发现它实际是创建一个核心线程数为nThreads,最大线程数为nThreads,存活有效时间为0,队列大小无限制的线程池。接下来我们模拟菜鸟驿站分发快递来展示下线程池具体执行效果。
首先我们创建一个测试线程池执行的方法:
static void run(ExecutorService pool) {
//创建15个任务提交给线程池去调度执行
for (int i = 1; i < 16; i++) {
final int n = i;
System.out.println("用户下单第" + i + "个快件。。。");
pool.execute(new Runnable() {
public void run() {
System.out.println("快件" + n + "开始配送了。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("快件" + n + "配送结束了。。。");
}
});
}
// 打印运行中线程池状态
System.out.println("-------正式员工-快递员数量:" + ((ThreadPoolExecutor) pool).getCorePoolSize());
System.out.println("-------正式员工+临时工-快递员数量:" + ((ThreadPoolExecutor) pool).getActiveCount());
System.out.println("-------仓库剩余未发快件:" + ((ThreadPoolExecutor) pool).getQueue().size());
try {
//等待任务都执行完毕
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印运行中线程池状态
System.out.println("-------正式员工-快递员数量:" + ((ThreadPoolExecutor) pool).getCorePoolSize());
System.out.println("-------正式员工+临时工-快递员数量:" + ((ThreadPoolExecutor) pool).getActiveCount());
System.out.println("-------仓库剩余未发快件:" + ((ThreadPoolExecutor) pool).getQueue().size());
}
接下来我们创建线程池去测试
public class ThreadPoolTest {
public static void main(String[] args) {
//创建一个核心线程数为5,最大活跃数为10,存活期为5秒,队列无界的线程池
ExecutorService pool=new ThreadPoolExecutor(5, 10,5L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
run(pool);
}
}
猜想一下执行结果是怎么样的呢?15个任务只有5个核心线程,是否会加开线程到最大活跃数10去执行任务呢?
实际输出结果:
用户下单第1个快件。。。
用户下单第2个快件。。。
用户下单第3个快件。。。
用户下单第4个快件。。。
快件1开始配送了。。。
快件2开始配送了。。。
用户下单第5个快件。。。
快件4开始配送了。。。
快件3开始配送了。。。
用户下单第6个快件。。。
用户下单第7个快件。。。
用户下单第8个快件。。。
用户下单第9个快件。。。
用户下单第10个快件。。。
用户下单第11个快件。。。
用户下单第12个快件。。。
用户下单第13个快件。。。
用户下单第14个快件。。。
用户下单第15个快件。。。
快件5开始配送了。。。
-------正式员工-快递员数量:5
-------正式员工+临时工-快递员数量:5
-------仓库剩余未发快件:10
快件4配送结束了。。。
快件2配送结束了。。。
快件3配送结束了。。。
快件8开始配送了。。。
快件5配送结束了。。。
快件9开始配送了。。。
快件1配送结束了。。。
快件7开始配送了。。。
快件6开始配送了。。。
快件10开始配送了。。。
快件8配送结束了。。。
快件9配送结束了。。。
快件11开始配送了。。。
快件12开始配送了。。。
快件10配送结束了。。。
快件7配送结束了。。。
快件6配送结束了。。。
快件15开始配送了。。。
快件14开始配送了。。。
快件13开始配送了。。。
快件11配送结束了。。。
快件12配送结束了。。。
快件15配送结束了。。。
快件14配送结束了。。。
快件13配送结束了。。。
-------正式员工-快递员数量:5
-------正式员工+临时工-快递员数量:0
-------仓库剩余未发快件:0
居然发现实际上只有五个核心线程在执行,其他的都缓存在队列中,查看线程池execute方法,我们会轻松找到答案:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//首先会判断任务数小于核心线程数,则创建线程去执行
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//再去看队列是否已满,未满则加入队列等待执行
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//最后才去加开线程去执行,如果加开失败则拒绝任务执行
else if (!addWorker(command, false))
reject(command);
}
为了验证上述思路,我们再测试一种场景:
public class ThreadPoolTest {
public static void main(String[] args) {
//创建一个核心线程数为5,最大活跃数为10,存活期为5秒,队列大小为3的线程池
ExecutorService pool = new ThreadPoolExecutor(5, 10, 5L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("------仓库已满,不接收订单了------");
}
});
run(pool);
}
}
输出结果:
用户下单第1个快件。。。
用户下单第2个快件。。。
用户下单第3个快件。。。
快件1开始配送了。。。
快件2开始配送了。。。
用户下单第4个快件。。。
快件3开始配送了。。。
快件4开始配送了。。。
用户下单第5个快件。。。
用户下单第6个快件。。。
用户下单第7个快件。。。
用户下单第8个快件。。。
用户下单第9个快件。。。
快件5开始配送了。。。
用户下单第10个快件。。。
快件9开始配送了。。。
用户下单第11个快件。。。
快件10开始配送了。。。
用户下单第12个快件。。。
快件11开始配送了。。。
用户下单第13个快件。。。
快件12开始配送了。。。
用户下单第14个快件。。。
------仓库已满,不接收订单了------
用户下单第15个快件。。。
------仓库已满,不接收订单了------
-------正式员工-快递员数量:5
快件13开始配送了。。。
-------正式员工+临时工-快递员数量:10
-------仓库剩余未发快件:3
快件1配送结束了。。。
快件2配送结束了。。。
快件7开始配送了。。。
快件3配送结束了。。。
快件8开始配送了。。。
快件4配送结束了。。。
快件6开始配送了。。。
快件5配送结束了。。。
快件12配送结束了。。。
快件13配送结束了。。。
快件9配送结束了。。。
快件10配送结束了。。。
快件11配送结束了。。。
快件7配送结束了。。。
快件8配送结束了。。。
快件6配送结束了。。。
-------正式员工-快递员数量:5
-------正式员工+临时工-快递员数量:0
-------仓库剩余未发快件:0
可以看到有两个任务被拒绝了,且加开了5个线程去执行,此时最大可执行任务为:最大活跃线程数10+队列中任务数3=13
2、Executors.newCachedThreadPool
查看源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
实际上是创建了一个核心线程数为0,最大活跃数为Integer.MAX_VALUE,存活时间为60秒,缓存队列为同步队列的线程池。SynchronousQueue没有缓冲区,即不会有任务在队列中排队,当任务加入时都会交给一个可用的线程去执行,没有则创建线程去执行,每个未销毁线程都可有效重复利用。
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
run(pool);
}
}
输出结果:
用户下单第1个快件。。。
用户下单第2个快件。。。
快件1开始配送了。。。
用户下单第3个快件。。。
快件2开始配送了。。。
用户下单第4个快件。。。
快件3开始配送了。。。
用户下单第5个快件。。。
快件4开始配送了。。。
用户下单第6个快件。。。
快件5开始配送了。。。
用户下单第7个快件。。。
快件6开始配送了。。。
用户下单第8个快件。。。
快件7开始配送了。。。
用户下单第9个快件。。。
快件8开始配送了。。。
用户下单第10个快件。。。
快件9开始配送了。。。
用户下单第11个快件。。。
快件10开始配送了。。。
用户下单第12个快件。。。
快件11开始配送了。。。
用户下单第13个快件。。。
快件12开始配送了。。。
用户下单第14个快件。。。
快件13开始配送了。。。
用户下单第15个快件。。。
快件14开始配送了。。。
-------正式员工-快递员数量:0
快件15开始配送了。。。
-------正式员工+临时工-快递员数量:15
-------仓库剩余未发快件:0
快件4配送结束了。。。
快件2配送结束了。。。
快件5配送结束了。。。
快件7配送结束了。。。
快件1配送结束了。。。
快件11配送结束了。。。
快件13配送结束了。。。
快件15配送结束了。。。
快件3配送结束了。。。
快件6配送结束了。。。
快件10配送结束了。。。
快件12配送结束了。。。
快件8配送结束了。。。
快件9配送结束了。。。
快件14配送结束了。。。
-------正式员工-快递员数量:0
-------正式员工+临时工-快递员数量:0
-------仓库剩余未发快件:0
3、Executors.newSingleThreadExecutor
查看源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
这里是创建了一个固定大小为1,存活时间为0,队列为阻塞队列的线程池,即单线程执行任务,且任务按先进先出原则顺序执行。这个就不再执行代码了。
4、Executors.newScheduledThreadPool
查看源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
创建一个指定核心线程数,最大活跃数不限,存活时间为0的支持定时任务与周期性任务的线程池。这里队列为延时队列,即任务加入队列中在有效时间未到前取不出来。
1)定时执行
示例代码:
public class ThreadPoolTest {
public static void main(String[] args) {
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(5);
pool.schedule(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + "任务执行了");
}
}, 3000, TimeUnit.MILLISECONDS);//3秒后执行
System.out.println(System.currentTimeMillis() + "提交定时任务");
}
}
输出结果:
1585750251873提交定时任务
1585750254873任务执行了
2)周期性执行
示例代码:
public class ThreadPoolTest {
public static void main(String[] args) {
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(5);
pool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + "任务执行了");
}
}, 2000, 1000, TimeUnit.MILLISECONDS);// 2秒后开始执行,每次间隔1秒
System.out.println(System.currentTimeMillis() + "提交周期性任务");
}
}
输出结果:
1585750905935提交周期性任务
1585750907936任务执行了
1585750908936任务执行了
1585750909936任务执行了
这里pool.scheduleWithFixedDelay方法也可以打到同样的周期性知晓效果,这两个方法的区别就在于当执行任务消耗时间大于间隔时间时的延时策略不同,具体看代码演示:
public class ThreadPoolTest {
public static void main(String[] args) {
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(5);
pool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);//执行任务所需时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + "任务执行了");
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
System.out.println(System.currentTimeMillis() + "提交周期性任务");
}
}
输出结果:
1585751235843提交周期性任务
1585751240844任务执行了
1585751243844任务执行了
1585751246844任务执行了
1585751249844任务执行了
我们发现scheduleAtFixedRate执行的时候,如果任务消耗时间大于间隔时间,会在上一个任务执行完后立即执行下一次任务。
public class ThreadPoolTest {
public static void main(String[] args) {
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(5);
pool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);//执行任务所需时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + "任务执行了");
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
System.out.println(System.currentTimeMillis() + "提交周期性任务");
}
}
输出结果:
1585751436804提交周期性任务
1585751441804任务执行了
1585751445806任务执行了
1585751449807任务执行了
这里实际相隔时间为4秒,可以看出,scheduleWithFixedDelay执行周期性任务时,当任务消耗时间大于间隔时间,则会在上一次任务执行完毕后开始计算间隔时间再开始执行下一次任务。
这两种区别在用起来时就要看对应的业务需求了。