线程池概念
本身程序在运行期间,创建和回收线程是比较消耗CPU性能的,所以为了减少这种消耗,就有了线程池的概念,即一个线程的任务执行完毕了,该线程不会立即释放,如果要释放也要等到满足条件释放。
PS:以前面试的时候会问线程池好处,千万不要回答减少线程数,线程池反而会增加线程。
线程池参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:核心线程数,就是线程池中一直常驻的线程,即使没有任务,这几个线程也一直存在
maximumPoolSize:线程池最大数量,字面意思就是允许这个线程池有最大数量的线程池,该值减去核心线程数=非核心线程数
keepAliveTime:非核心线程数的存活时间,即没有任务的时候,该线程被释放的时间
TimeUnit:上述存活时间单位,毫秒、秒等
BlockingQueue:阻塞队列,当任务进来的时候,如果发现核心线程数没有空的 都被占用了,任务就会进去阻塞队列中,等待核心线程有空闲
ThreadFactory:线程工厂,创建线程用的,一般我们不用关注
RejectedExecutionHandler:拒绝策略,什么意思呢?任务进来的时候,如果核心线程被占满了,这时候进入阻塞队列,但是阻塞队列也满了,这时候就开辟非核心线程,但是非核心线程也不够了(即非核心线程+核心线程=max线程数,超出预期了),就执行拒绝策略
目前有四种拒绝策略:
1.AbortPolicy
这是默认的拒绝策略。当任务被拒绝时,会抛出“RejectedExecutionException”异常。
2.CallerRunsPolicy
当任务被拒绝时,直接在调用线程中运行被拒绝的任务。如果线程池已经关闭,任务将被丢弃。
3.DiscardOldestPolicy
当任务被拒绝时,丢弃队列中最旧的未处理任务,然后尝试重新提交被拒绝的任务。
4.DiscardPolicy
当任务被拒绝时,直接丢弃任务,不会抛出任何异常。
线程池执行流程图
注意一点,当核心线程数满了后,后面进来的任务不一定执行的是先来先执行的,因为可能A任务先到,但A进去了阻塞队列,B任务到的时候,阻塞队列已经满了,所以优先开辟非核心线程 立马执行任务;如果不太懂的话,下面会有一些代码和demo能更好理解
传统几个线程池
这边主要介绍一下几个官方定义好的线程池,当然构造函数只是举个例子,因为有些线程池的构造函数有多个。
大部分学习或者工作的情况下都用不到,因为这些线程池的局限性很高,比如阻塞队列无限、最大线程数无限,容易导致应用或者服务器oom,阿里官方开发手册中也禁止开发人员使用ExecutorService的线程池,所以我们尽量自己定义线程池参数,更好满足项目需求
1.FixedThreadPool:一个固定大小的线程池,可以用于执行固定数量的任务,在任何时候都有固定数量的线程处于活动状态。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以看到,该线程池可以定义核心线程和非核心线程池,参数都是nThreads,所以该线程池没有非核心线程数,即来一个任务要么进入核心线程执行,要么进入阻塞队列等待。
2.CachedThreadPool:一个会根据需要创建新线程的线程池,可以重用以前创建的线程,如果线程有一段时间(60秒)未被使用,则会自动被终止并移除。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到,该线程池没有核心线程,允许有无限大的非核心线程数,同时注意一下阻塞队列SynchronousQueue,这个队列是一个很特殊的阻塞队列,没有容量,不持有任何任务,学有余力的同学可以学习一下这个队列的源码,这里说明一下这个队列是即进即出的,类似消费者生产则同步模型。
简而言之,该线程池就是来一个任务就执行任务,完全没有上面任务流程图任何杂七杂八的分支逻辑,因为没有核心线程数和最大线程数量,也没有阻塞队伍容量,来A任务,好,我执行;来B任务了,好,我也执行;来C任务,ok 我继续执行。该线程池适用于一个时间段内的大量同步任务。
3.SingleThreadExecutor:一个单线程的线程池,它确保所有任务在一个序列中按顺序进行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
单线程的线程池,核心线程数=1,最大线程数=1,阻塞队列默认无限大,进来的线程都是按顺序执行的
4.ScheduledThreadPool:一个可以安排在给定延迟后运行的定时线程池。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
相关api:schedule、scheduleAtFixedRate,本质上还是一个线程池,只不过增加了一个定时器
一些相关demo以及参数组合示例
为了更好的理解,写了一些demo来理解一下线程池,这边定义了一下线程任务执行是两秒
public class SingleThread extends Thread {
String name = "";
SingleThread(String threadName) {
name = threadName;
}
SingleThread(){
}
@Override
public void run() {
super.run();
try {
Thread.sleep(2000);
LocalDateTime now = LocalDateTime.now();
System.out.println("线程运行,当前线程:"+name+",时间:"+now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
demo1:自定义核心、最大线程数、阻塞队列
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);
ExecutorService pool = new ThreadPoolExecutor(
2, //corePoolSize = 0
5, //maximumPoolSize
5L, //keepAliveTime
TimeUnit.SECONDS,
queue,
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Thread t1 = new SingleThread("t1");
Thread t2 = new SingleThread("t2");
Thread t3 = new SingleThread("t3");
Thread t4 = new SingleThread("t4");
Thread t5 = new SingleThread("t5");
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
//关闭线程池
pool.shutdown();
运行结果:
好理解吧,这五个任务我们可以看过一瞬间一起要执行了。这边线程池定义了核心线程数=2,最大线程数=5,阻塞队列大小=1;所以任务1、2立马进入核心线程执行,任务3进来的时候,需要进入阻塞队列了,4、5任务来的时候,阻塞队列也满了,所以没办法,尝试开辟非核心线程,发现还有空余创建线程,4、5线程就立马执行了,所以看到为什么1、2、4、5一起执行了,3线程等待了2秒后才执行(因为需要等核心线程释放任务)
demo2:一些拒绝策略
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);
ExecutorService pool = new ThreadPoolExecutor(
2, //corePoolSize = 0
3, //maximumPoolSize
5L, //keepAliveTime
TimeUnit.SECONDS,
queue,
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Thread t1 = new SingleThread("t1");
Thread t2 = new SingleThread("t2");
Thread t3 = new SingleThread("t3");
Thread t4 = new SingleThread("t4");
Thread t5 = new SingleThread("t5");
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
//关闭线程池
pool.shutdown();
}
执行结果
抛出异常了,很容易理解,t1和t2进入核心线程执行任务,t3进入了阻塞队列,t4开辟了非核心线程,t5创建线程的时候超过最大线程数了,所以拒绝策略触发,AbortPolicy抛出异常。
这边主要测试一下CallerRunsPolicy和DiscardOldestPolicy
CallerRunsPolicy如上文所说:会直接在调用者线程中运行这个任务。如果调用者线程正在执行一个任务,则会创建一个新线程来执行被拒绝的任务。什么意思呢?我也不懂,写demo实践一下。我们看下改了拒绝策略后,执行结果如下:
查阅相关资料后,被拒绝的任务,会在调用提交任务的的线程调用,所以可能是主线程(大部分情况)也可能是线程调用;这边增加一下线程写一个比较复杂的场景,可以参考一下,代码如下:
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);
ExecutorService pool = new ThreadPoolExecutor(
2, //corePoolSize = 0
3, //maximumPoolSize
5L, //keepAliveTime
TimeUnit.SECONDS,
queue,
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
Thread t1 = new SingleThread("t1");
Thread t2 = new SingleThread("t2");
Thread t3 = new SingleThread("t3");
Thread t4 = new SingleThread("t4");
Thread t5 = new SingleThread("t5");
Thread t6 = new SingleThread("t6");
Thread t7 = new SingleThread("t7");
Thread t8 = new SingleThread("t8");
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
LocalDateTime now1 = LocalDateTime.now();
System.out.println("t5 start,当前时间:"+now1.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
pool.execute(t5); //开始执行任务,卡住下面主进程
LocalDateTime now2 = LocalDateTime.now();
System.out.println("t5 end,当前时间:"+now2.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
pool.execute(t6);
pool.execute(t7);
LocalDateTime now3 = LocalDateTime.now();
System.out.println("t8 start,当前时间:"+now3.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
pool.execute(t8);
LocalDateTime now4 = LocalDateTime.now();
System.out.println("t8 end,当前时间:"+now4.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
//关闭线程池
pool.shutdown();
}
执行的一种结果如下图:
这里我们一步一步来分析,首先t5start的log很容易了解,因为这是主线程,前面四个t1、t2、t3、t4任务进去还没执行好呢,
1.首先t1和t2肯定首先进入了核心队列,开始执行
2.t3进入阻塞队列,需要等待核心线程有空余
3.t4任务进入,开辟非核心线程开始执行任务
4.t5任务进入,发现没有线程可用,执行拒绝策略,开始主线程执行该任务,并阻塞住主线程
所以上面四步可以解释为什么t1/2/4/5这四个任务在同一时间执行完毕,执行完毕时,打印log t5 end,下一步分析
5.这时候678的提交任务的代码开始执行,t5end的log打印完毕的时候,1、2(核心线程)和4(非核心线程)和5(主线程)执行完毕了
6.t3发现有空闲线程了,执行,进来的6和7(7任务其实在主线程中执行了 做了拒绝策略,这个日志情况比较极端,7执行的时候,其实前面空闲线程还没有,其实是一瞬间的事情,后面测试的时候延迟1ms推7任务,主线程不会阻塞并且7进了线程池执行)任务也发现有空闲线程,也开始执行,所以367任务同一个时间结束,8进入的时候,发现没有可用线程了,所以进入堵塞队列,等待前面任务执行完毕空出线程。
总结一下该拒绝策略:不会丢失任何任务,所以也不算拒绝策略
DiscardOldestPolicy如上文所说:会丢弃队列中最老的未处理任务,然后重新尝试执行任务。我们看下改了拒绝策略后,执行结果如下图:
很容易理解,t3任务没了,因为此时t3在队列中,也还没有处理,所以丢弃了t3
自定义拒绝策略
自定义拒绝策略可以更灵活
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);
ExecutorService pool = new ThreadPoolExecutor(
2, //corePoolSize = 0
3, //maximumPoolSize
0L, //keepAliveTime
TimeUnit.MILLISECONDS,
queue,
Executors.defaultThreadFactory(),
new MyAbortPolicy());
Thread t1 = new SingleThread("t1");
Thread t2 = new SingleThread("t2");
Thread t3 = new SingleThread("t3");
Thread t4 = new SingleThread("t4");
Thread t5 = new SingleThread("t5");
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
//关闭线程池
pool.shutdown();
}
public static class MyAbortPolicy implements RejectedExecutionHandler {
public MyAbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
//举个例子,这里被拒绝的线程,重新开一个线程继续执行
new Thread(r).start();
}
}
demo3:阻塞队列
这里主要写一个特殊的SynchronousQueue<>()队列,其实该队列上面说过了,此队列没有容量,即进即出,所以使用这个队列的线程池,如果核心线程不够了,都会开启新的非核心线程
SynchronousQueue<Runnable> queue1 = new SynchronousQueue<>();
ExecutorService pool = new ThreadPoolExecutor(
2, //corePoolSize = 0
3, //maximumPoolSize
10L, //keepAliveTime
TimeUnit.SECONDS,
queue1,
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
Thread t1 = new SingleThread("t1");
Thread t2 = new SingleThread("t2");
Thread t3 = new SingleThread("t3");
Thread t4 = new SingleThread("t4");
Thread t5 = new SingleThread("t5");
Thread t6 = new SingleThread("t6");
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4); //拒绝策略
pool.execute(t5);
try {
Thread.sleep(1);
} catch (Exception e) {
}
pool.execute(t6);
//关闭线程池
pool.shutdown();
}
执行结果如下图:
解释一下:1、2任务进入核心线程执行,3进入阻塞队列开辟非核心线程执行,4任务进来,执行拒绝策略,主线程执行然后堵塞了2秒,两秒后5、6任务开始进入线程池,发现有空闲线程,执行。这里延迟了1ms主要是为了保证前面1和2任务能够完全执行完毕,否则任务5又会执行拒绝策略
demo4:官方线程池
不写了,比较容易理解,因为参数都是固定的
线程池使用场景
后续更新……
常见错误点
1.阻塞队列的任务只有等核心线程空出来才会出队列执行任务。
这是错误的,阻塞队列执行的消费者生产者模型,只要线程池中有空闲线程或者能开辟线程就能出队列执行
2.任务进入线程池会优先使用核心线程,会优先判断核心线程
错误,任务进入线程池,优先会判断线程池有没有空闲线程,不管它是核心线程还是非核心线程,只要是空闲线程就使用它,如果没有空闲线程,才尝试判断核心线程等等步骤来开辟新线程