线程学习:https://blog.csdn.net/qq_42082278/article/details/107465102
1、线程池的实现原理
下图所示为线程池的实现原理:调用方不断地向线程池中提交任务;线程池中有一组线程,不断从队列中取任务,这是一个典型的生产者-消费者模型.
要实现这样一个线程池,有几个问题需要考虑:
- 队列设置多长?如果是无界的,调用方不断地往队列中放任务,可能导致内存耗尽。如果是有界的,当队列满了后,调用方如何处理
- 线程池中的线程个数是固定的,还是动态变化的?
- 每次提交新任务,是放入队列?还是开新线程?
- 当没有任务的时候,线程时睡眠一段时间 ?还是进入阻塞?如果进入阻塞,如何唤醒?
2、线程池的类继承体系
线程池的类继承体系如下图所示:
在这里有两个核心的类:ThreadPoolExector 和 ScheduledThreadPoolExecutor ,后者不仅可以执行某个任务,还可以周期性地执行任务。
向线程池中提交的每个任务,都必须实现Runnable 接口,通过最上面的Executor接口中的 execute(Runnable command) 向线程池提交任务、
然后,在ExecutorService中,定义了线程池的关闭接口shutdown(),还定义了可以有返回值的任务,也就是Callable.
3、ThreadPoolExecutor
基于线程池的实现原理,下面看一下ThreadPoolExector的核心数据结构。
public class ThreadPoolExecutor extends AbstractExecutorService {
//...
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 存放任务的阻塞队列
private final BlockingQueue<Runnable> workQueue;
// 对线程池内部各种变量进行互斥访问控制
private final ReentrantLock mainLock = new ReentrantLock();
// 线程集合
private final HashSet<Worker> workers = new HashSet<Worker>();
//...
}
每一个线程是一个Worker对象。Worker是ThreadPoolExector的内部类,核心数据结构如下:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
// ...
final Thread thread; // Worker封装的线程
Runnable firstTask; // Worker接收到的第1个任务
volatile long completedTasks; // Worker执行完毕的任务个数
// ...
}
有定义会发现,Worker继承于AQS,也就是是说Worker本身就是一把锁。这把锁有什么用处呢?用于线程池的关闭、线程执行任务的过程中。
4、核心配置参数解释
ThreadPoolExecutor在其构造方法中提供了几个核心配置参数,来配置不同的策略的线程池。
上面的参数解释
- corePoolSize:在线程池中始终维护的线程个数。
- maxPoolSize:在corePoolSize已满,队列也满的情况下,扩充线程至此值。
- keepAliveTime/TimeUnit:maxPoolSize中的空闲线程,销毁所需要的时间,总线程数收缩回corePoolSize.
- blockingQueue:线程池所用的队列类型。
- threadFactory:线程创建工厂,可以自定义,有默认值(Executors.defaultThreadFactory() ).
- RejectedExecutionHandler:corePoolSize已满,队列已满,maxPoolSize已满,最后的拒绝策略。
下面来看6个配置参数在任务的提交过程中是怎么运作的。在每次往线程池中提交任务的时候,有如下的处理流程。
步骤一:判断当前线程数是否大于或等于corePoolSize。如果小于则新建线程执行;如果大于,则进入步骤二。
步骤二:判断队列是否已满。如未满,则放入;如已满,则进入步骤三。
步骤三:判断当前线程数是否大于或等于maxPoolSize。如果小于,则新建线程执行;如果大于则进入步骤四。
步骤四:根据拒绝策略,拒绝任务。
总结:首先判断corePoolSize,其次判断blockingQueue是否已满,接着判断maxPoolSize,最后使用拒绝策略。
很显然,基于这种流程,如果队列是无界的,将永远没有机会走到步骤三,也即maxPoolSize没有使用,
也一定不会走到步骤四。
5、 线程池的优雅关闭
线程池的关闭,较之前的关闭更加复杂。当关闭一个线程池的时候,有的线程还在执行某个任务,有的调用正在向线程池提交任务,并且队列中可能还有为执行的任务。因此,关闭过程不可能是瞬间的,而是需要一个平滑的过渡,这就是涉及线程池的完整生命周期管理。
5.1 线程池的生命周期
在JDK 7 中线程数量(WorkCount)和线程池状态(runState)这两个变量打包存储在一个字段里面,即ctl变量。如下图所示,最高的3位存储线程池状态,其余29位存储线程个数。而在JDK6中,这两个变量时分开存储的。
由上面的代码可以看到,ctl变量被拆成两半,最高的3位用来表示线程池的状态,低的29位表示线程的个数。线程池的状态有五种,分别是RUNNING、SHUTDOWN、STOP、TYDYING、TERMINATED、
下面分析状态之间的迁移过程,如图所示:
线程池有两个关闭方法,shutdown()和shutdownNow(),这两个方法会让线程池切换到不同的状态。在队列为空。线程池也为空之后,进入TIDYING状态;最后执行一个钩子方法terminated(),进入TERMINAED状态,线程池才真正关闭。
这里的状态迁移有个非常关键的特征:从小到大迁移。-1,0,1,2,3。只会从小的状态值往大的状态值迁移,不会逆向迁移。例如,当线程池的状态在TIDYING=2时,接下来只可能迁移到TERMINATED=3,不可能迁回STOP=1或者其他状态。
除terminated() 之外,线程池还提供了其他几个钩子方法,这些方法的显示都是空的。如果想实现自己的线程池,可以重写这几个方法。
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }
5.1 正确关闭线程池的步骤
关闭线程池的过程为:在调用 shutdown()或者shutdownNow()之后,线程池并不会立即关闭,接下来需要调用 awaitTermination() 来等待线程池关闭。关闭线程池的正确步骤如下:
// executor.shutdownNow();
executor.shutdown();
try {
boolean flag = true;
do {flag = ! executor.awaitTermination(500, TimeUnit.MILLISECONDS); }
while (flag);
} catch (InterruptedException e) {
// ...
}
awaitTermination(…)方法的内部实现很简单,如下所示。不断循环判断线程池是否到达了最终状态TERMINATED,如果是,就返回;如果不是,则通过termination条件变量阻塞一段时间,之后继续判断。
5.3 .shutdown()与shutdownNow()的区别
- shutdown()不会清空任务队列,会等所有任务执行完成,shutdownNow()清空任务队列。
- shutdown()只会中断空闲的线程,shutdownNow()会中断所有线程。
下面看一下在上面的代码里中断空闲线程和中断所有线程的区别。
关键区别点在tryLock():一个线程在执行一个任务之前,会先加锁,这意味着通过是否持有锁,可以判断出线程是否处于空闲状态。tryLock()如果调用成功,说明线程处于空闲状态,向其发送中断信号;否则不发送。
tryLock()方法
6、任务的提交过程分析
提交任务的方法如下:
7、任务的执行过程分析
8、线程池的4种拒绝策略
在execute(Runnable command)的最后,调用了reject(command)执行拒绝策略,代码如下所示:
RejectedExecutionHandler 是一个接口,定义了四种实现,分别对应四种不同的拒绝策略,默认是AbortPolicy。
四种策略的实现代码如下:
策略1:调用者直接在自己的线程里执行,线程池不处理,比如到医院打点滴,医院没地方了,到你家自己操作吧:
策略2:线程池抛异常:
策略3:线程池直接丢掉任务,神不知鬼不觉:
策略4:删除队列中最早的任务,将当前任务入队列:
9、创建线程池
Java 创建线程池的方法有:
1、 通过 ThreadPoolExcetor 或者 ScheduledThreadPoolExcutor 。
2、通过Executors 的工具类创建四个java默认的线程池、
但是阿里使用手册明确表示线程池不能使用Executors创建。
因为 :1、默认的线程池名字是默认的不方便排查。
2、底层使用的无限队列,会造成系统内存过大,或者系统假死。
从图中可以看到 线程的名字生成规则、
代码很简单,就是
package com.lagou.threadpool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author xibanqiu
* created by sheting on 2021/4/21
*/
public class ThreadPoolTest {
public static class Task extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public static class MyThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
MyThreadFactory(String name){
SecurityManager securityManager = System.getSecurityManager();
group = (securityManager != null)?securityManager.getThreadGroup():
Thread.currentThread().getThreadGroup();
namePrefix = name + "-thread-";
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(group,r,namePrefix + threadNumber ,0);
if(t.isDaemon()){
t.setDaemon(false);
}
if(t.getPriority() != Thread.NORM_PRIORITY){
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5,
7,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
new MyThreadFactory("自定义线程池名称"));
for (int i = 0; i <5 ; i++) {
threadPoolExecutor.execute(new Task());
}
threadPoolExecutor.shutdown();
}
}
10、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor实现了按时间调度来执行任务:
-
延迟执行任务
-
周期执行任务
区别如下:
AtFixedRate:按固定频率执行,与任务本身执行时间无关。但有个前提条件,任务执行时间必须小于间隔时间,例如间隔时间是5s,每5s执行一次任务,任务的执行时间必须小于5s。
WithFixedDelay:按固定间隔执行,与任务本身执行时间有关。例如,任务本身执行时间是10s,间隔2s,则下一次开始执行的时间就是12s。