概述
线程是一种稀缺的资源,每个线程的创建需要分配部分内存作为栈空间,所以如果每个任务都创建一个新的线程容易导致内存溢出,同时CPU的时间片有限,大量线程涌出会导致频繁的线程上下文切换问题,于是产生了线程池
合理使用线程池能够带来三个好处:
- 降低资源消耗,通过重复利用已有的线程,降低线程创建和销毁造成的资源消耗
- 提高响应速度,当任务到达时,可以直接在已有的线程上执行,不需要等待线程创建的时间
- 提高线程的可管理性,线程时稀缺资源,无限制的创建不光会消耗系统资源,而且会影响系统的稳定性,使用线程池可以对所有线程进行统一分配、调优和监控
线程池的组成
- 核心线程:包含指定数量的核心线程数,需要执行任务时,由这些线程来处理
- 阻塞队列:当线程池中的线程达到最大线程数,任务会进入阻塞队列排队,等待空闲线程的出现
- 拒绝策略:当线程池满了,同时阻塞队列也满了,再来新的任务就需要进行拒绝,怎么拒绝任务、拒绝哪些任务就是拒绝策略需要规定的
自定义线程池
/***
* @author shaofan
* @Description 自定义线程池
*/
@Slf4j
public class ThreadPoolTest{
public static void main(String[] args) throws InterruptedException {
// 如果队列已满,则死等队列有空闲
RejectPolicy<Runnable> alwaysWait = (queue,task)->{queue.put(task);};
// 如果队列已满,则等待1s
RejectPolicy<Runnable> waitSec = (queue,task)->{queue.offer(task,1,TimeUnit.SECONDS);};
// 如果队列已满,则放弃入队
RejectPolicy<Runnable> giveUp = (queue,task)->{};
// 如果队列已满,则抛出异常
RejectPolicy<Runnable> error = (queue,task)->{throw new RuntimeException("队列已满");};
// 如果队列已满,则由调用线程自己执行
RejectPolicy<Runnable> runByMySelf = (queue,task)->{task.run();};
MyThreadPool threadPool = new MyThreadPool(2,1000,TimeUnit.MILLISECONDS,10,error);
for (int i = 0; i < 20; i++) {
int j = i;
threadPool.execute(()->{
try {
Thread.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}",j);
});
}
}
}
/***
* 拒绝策略接口,策略模式,定义队列满了之后的操作,与入队操作解耦
*/
@FunctionalInterface
interface RejectPolicy<T>{
/**
* 当队列满了的拒绝操作
* @param queue
* @param task
*/
void reject(BlockingQueue<T> queue,T task) throws InterruptedException;
}
/***
* 自定义线程池
*/
@Slf4j
class MyThreadPool {
/**
* 任务队列
*/
private BlockingQueue<Runnable> taskQueue;
/**
* 线程容器
*/
private HashSet<Worker> workers = new HashSet<>();
/**
* 核心线程数
*/
private int coreSize;
/**
* 任务超时时间
*/
private long timeout;
/**
* 任务超时时间单位
*/
private TimeUnit unit;
/**
* 拒绝策略
*/
private RejectPolicy<Runnable> rejectPolicy;
public MyThreadPool(int coreSize,int timeout,TimeUnit unit,int queueCapacity,RejectPolicy<Runnable> rejectPolicy){
this.coreSize = coreSize;
this.timeout = timeout;
this.unit = unit;
this.taskQueue = new BlockingQueue<>(queueCapacity);
this.rejectPolicy = rejectPolicy;
}
/**
* 执行任务
* @param task
*/
public void execute(Runnable task) throws InterruptedException {
synchronized (workers){
// 如果线程池中的线程数没有达到核心线程数,则创建新的线程
if(workers.size()<coreSize){
Worker worker = new Worker(task);
log.debug("新增worker-{},{}",worker,task);
workers.add(worker);
worker.start();
}else{
// 如果线程数达到了核心线程数,则将任务存入任务队列中
log.debug("任务入队-{}",task);
taskQueue.tryPut(rejectPolicy,task);
}
}
}
/***
* 线程池中的线程
*/
class Worker extends Thread{
private Runnable task;
Worker(Runnable task){
this.task = task;
}
@SneakyThrows
@Override
public void run() {
// 当任务不为空,则执行任务;当任务为空,则从任务队列中取得新的任务
while(task != null || (task = taskQueue.take()) != null){
try{
log.debug("正在执行{}",task);
task.run();
}catch (Exception e){
e.printStackTrace();
}finally {
task = null;
}
}
// 如果任务队列中也没有新的任务了,将当前线程从线程池中移除
synchronized (workers){
log.debug("worker 被移除{}",this);
workers.remove(this);
}
}
}
}
/***
* 阻塞队列,任务会进入阻塞队列有线程读取
* @param <T>
*/
@Slf4j
class BlockingQueue<T>{
/**
* 任务队列
*/
private Deque<T> queue = new ArrayDeque<>();
/**
* 任务锁,多个线程从队列中读取任务时,保证线程安全
*/
private ReentrantLock lock = new ReentrantLock();
/**
* 生产者条件变量,队列满时不能生产,生产线程等待
*/
private Condition fullWaitSet = lock.newCondition();
/**
* 消费者条件变量,队列为空时不能消费,消费线程等待
*/
private Condition emptyWaitSet = lock.newCondition();
/**
* 容量
*/
private int capacity;
public BlockingQueue(int capacity){
this.capacity = capacity;
}
/***
* 阻塞超时的获取任务
* @param timeout
* @param unit
* @return
* @throws InterruptedException
*/
public T poll(long timeout, TimeUnit unit) throws InterruptedException {
lock.lock();
try{
long nanos = unit.toNanos(timeout);
while(queue.isEmpty()){
// 以剩余的时间作为退出等待的条件
if(nanos<=0){
return null;
}
// 获取剩余的时间
nanos = emptyWaitSet.awaitNanos(nanos);
}
T t = queue.removeFirst();
// 唤醒生产线程
fullWaitSet.signalAll();
return t;
}finally {
lock.unlock();
}
}
/**
* 从队列中阻塞获取
* @return
* @throws InterruptedException
*/
public T take() throws InterruptedException {
lock.lock();
try{
while(queue.isEmpty()){
emptyWaitSet.await();
}
T t = queue.removeFirst();
// 唤醒生产线程
fullWaitSet.signalAll();
return t;
}finally {
lock.unlock();
}
}
/**
* 阻塞添加任务
* @param task
* @throws InterruptedException
*/
public void put(T task) throws InterruptedException {
lock.lock();
try{
while(queue.size()==capacity){
log.debug("等待加入任务队列");
fullWaitSet.await();
}
queue.addLast(task);
// 唤醒消费线程
emptyWaitSet.signalAll();
}finally {
lock.unlock();
}
}
/**
* 带有超时时间的阻塞添加
* @param task
* @param timeout
* @param unit
* @return
* @throws InterruptedException
*/
public boolean offer(T task,long timeout,TimeUnit unit) throws InterruptedException {
lock.lock();
try{
long nanos = unit.toNanos(timeout);
while(queue.size()==capacity){
if(nanos<=0){
return false;
}
log.debug("等待加入任务队列");
nanos = fullWaitSet.awaitNanos(nanos);
}
queue.addLast(task);
// 唤醒消费线程
emptyWaitSet.signalAll();
return true;
}finally {
lock.unlock();
}
}
/**
* 带有拒绝策略的入队
* @param rejectPolicy
* @param task
*/
public void tryPut(RejectPolicy<T> rejectPolicy,T task){
lock.lock();
try{
try{
// 队列已满,执行拒绝策略定义的操作
if(queue.size() == capacity){
rejectPolicy.reject(this,task);
}else{
//队列还有空闲直接入队
log.debug("任务入队-{}",task);
queue.addLast(task);
emptyWaitSet.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
public int size(){
return queue.size();
}
}
ThreadPoolExecutor
线程池状态
ThreadPoolExecutor使int的高三位来表示线程池状态,低27位表示线程数量
- RUNNING:高三位为111,可以接收新任务和处理阻塞队列的任务,表示线程池正常工作中
- SHUTDOWN:高三位为000,不能接收新任务,可以处理阻塞队列的任务,线程池调用shutdown方法时表示关闭线程池,但shutdown会等线程池中的任务执行完毕再关闭
- STOP:高三位为001,不能接收新任务也不能处理阻塞队列的任务,线程池调用shutdownNow方法时,强制关闭线程,会中断正在执行当任务并抛弃任务队列的任务
- TIDYING:高三位为010,任务全部执行完毕,活动线程为0,即将进入终结
- TERMINATED:高三位为011,终结状态
从数字上比较,TERMINATED>TIDYING>SOP>SHUTDOWN>RUNNING
将线程状态和线程个数存储再一个原子变量ctl中,目的是将线程池状态与线程个数合二为一,这样就可以用一次cas操作进行赋值
线程池的创建
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程数,当线程池中的线程没有达到这个数量,每来一个任务就会创建一个线程,不论当前是否能够直接处理这个任务,当线程数达到这个数量才会优先使用已有的线程
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量,如果队列满了并且已创建的线程数小于这个值,则线程池会再创建新的救急线程来执行这个任务;如果阻塞队列是无限的这个参数就没有效果
- keepAliveTime:针对救急线程(核心线程以外的线程)的空闲存活时间,当救急线程空闲后,超过这个时间会被释放
- unit:keepAliveTime的时间单位
- workQueue:阻塞队列,用于保存等待执行任务的队列,当线程数达到了核心线程数时,新来的任务不会直接创建线程执行,而是进入阻塞队列
- ThreadFactory:创建线程的线程工厂,可以通过线程工厂来给每个创建出来的线程创建更有意义的名字,代码:
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build()
(框架guava) - handler:拒绝策略,当阻塞队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略来处理新来的任务
可选的阻塞队列
- ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,此队列按FIFO原则对数据进行排序
- LinkedBlockingQueue:一个基于连边结构的阻塞队列,此队列按FIFO排序元素,吞吐量要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须要等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool()使用了这个队列
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列
可选的拒绝策略
- AbortPolicy:直接抛出异常,默认策略
- CallerRunsPolicy:由调用方的线程来执行这个任务
- DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务
- DiscardPolicy:丢弃任务
- Dubbo的实现:再抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便定位问题
- Netty的实现:创建一个新的线程来执行任务
- ActiveMQ的实现:待超时等待(60s)尝试放入队列
- PinPoint的实现:使用了一个拒绝策略链,会注意尝试策略链中的每种拒绝策略
newFixedThreadPool(固定大小的线程池)
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads,nThreads,
0L,TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 核心线程数=最大线程数,没有救急线程,因此也无需超时时间
- 阻塞队列是无界的,可以方任意数量的任务
- 适用于任务量一致,相对耗时的任务
newCachedThreadPool
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,
60L,TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,意味着全部都是救急线程,救急线程可以无限创建
- 队列采用了SynchronousQueue实现,内部没有容量,每一个任务都会对应一个线程直接执行,没有空闲线程就创建救急线程
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(1,1,0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用场景:希望多个任务排队执行,线程数固定为1,任务数多余1时,会放入无界队列排队,任务执行完毕,这唯一的线程也不会被释放
区别
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何不久措施,而线程池还会新建一个线程,保证线程池的正常工作
- Executors.newSingleThreadExecutor()线程个数始终为1,不可更改;应用装饰器模式,对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecutor中特有的方法
- Executors.newFixedThread(1)初始为1,但还可以修改,对外暴露ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改
任务的提交
线程池中任务的提交可以通过execute、submit、invokeAll、invokeAny四种方法来执行
execute
execute用于提交不需要返回值的任务,所以不知道任务是否执行完成、什么时候执行完成
threadPool.execute(new Runnable() {
@Override
public void run() {
//任务代码
}
});
submit
submit用于提交有返回值的任务,得到一个Future类型的对象,可以通过future.get()来阻塞获取任务的返回值,或者通过future.get(long timeout,TimeUnit unit)来超时获取任务的返回值
Future<Boolean> future = threadPool.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return true;
}
});
log.debug(String.valueOf(future.get()));
invokeAll
invokeAll可提交多个任务,有两种重载的形式,一种是阻塞执行,另一种是带有超时时间的,带有超时时间的形式如果任务执行超时会抛出异常
List<Future<Object>> futures = threadPool.invokeAll(Arrays.asList(
()->{
return true;
},
()->{
Thread.sleep(2000);
return 1;
},
()->{
return "ok";
}
),1,TimeUnit.SECONDS);
futures.forEach(f->{
try {
log.debug("{}",f.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
invokeAny
invokeAny中所有任务都会被提交,但是只有一个能够成功执行,哪个任务先执行完毕,返回此任务执行结果,其他任务取消,同样也有带超时时间的重载方法
Object o = threadPool.invokeAny(Arrays.asList(
()->{
return true;
},
()->{
Thread.sleep(2000);
return 1;
},
()->{
return "ok";
}
));
log.debug("{}",o);
关闭线程池
shutdown
平和的关闭线程池,线程池状态变为SHUTDOWN,此时不会接收新的任务,但是会将已经提交的任务执行完,不会阻塞调用线程的执行
public void shutdown(){
final ReentranLock mainLock = this.mainLock;
mainLock.lock();
try{
checkShutdownAccess();
// 修改线程池状态
advanceRunState(SHUTDOWN);
// 打断空闲线程
interruptIdleWorkers();
onShutdown();// 子类ScheuledThreadPoolExecutor扩展点
}finally{
mainLock.unlock();
}
// 尝试进行终结,没有运行的线程可以立刻终结,如果还有
tryTerminate();
}
shutdownNow
立即关闭线程池,线程池的状态将变为STOP,此时不会接收新的任务,并且将队列中的任务返回,并用interrupt方法中断正在执行的任务
public List<Runnable> shutdownNow(){
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try{
checkShutdownAccess();
// 修改线程池的状态
advanceRunState(STOP);
// 打断所有线程
interruptWorkers();
// 获取队列中所有剩余任务
tasks.dranQueue();
}finally{
mainLock.unlock();
}
// 尝试终结
tryTerminate();
return tasks;
}
任务调度线程池-ScheduledThreadPoolExecutor
任务调度线程池用于完成任务调度功能(延时或定时执行任务),在任务调度线程池之前的任务调度都是使用Timer类来实现,Timer类简单易用,但是有一个明显的缺点:所有任务都是都同一个线程来调度,所以所有任务都是串行执行的,同一时间只有一个任务在执行,前一个任务的延迟或异常都会影响到后一个任务
Timer
@Slf4j
public final class Demo{
public static void main(String[] args){
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("first");
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("second");
}
};
timer.schedule(task1,1000L);
timer.schedule(task2,2000L);
}
}
ScheduledThreadPoolExecutor
延时执行
@Slf4j
public final class Demo{
public static void main(String[] args){
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
pool.schedule(()->{
log.debug("ok");
},2000L,TimeUnit.MILLISECONDS);
}
}
定时执行
@Slf4j
public final class Demo{
public static void main(String[] args){
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
pool.scheduleAtFixedRate(()->{
log.debug("ok");
},0L,1000L,TimeUnit.MILLISECONDS);
}
}
ScheduledThreadPoolExecutor应用-定时任务
在每周一零点执行操作,如数据备份等
@Slf4j
public class TestScheduled {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
// 构造执行任务的时间
LocalDateTime time = now.withHour(0).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.MONDAY);
// 如果本周四已经过去了,直接加一周到下周四一
if(now.compareTo(time)>0){
time = time.plusWeeks(1);
}
// 获取从现在到指定时间的时间间隔
long initDelay = Duration.between(now,time).toMillis();
// long initDelay = 0;
// 定时时间间隔,使用1000测试,正式场景使用一周的毫秒数
long period = 1000;
ScheduledExecutorService pool = new ScheduledThreadPoolExecutor(1);
pool.scheduleAtFixedRate(()->{
log.debug("operation");
},initDelay,period, TimeUnit.MILLISECONDS);
}
}
tomcat线程池
- LimitLatch用来限流,可以控制最大连接个数,类似JUC中的Semaphore
- Acceptor只负责接收新的socket连接
- Poller只负责监听socker channel是否有可读的I/O事件
- 一旦有可读事件发生,封装一个任务对象(socketProcessor),提交给Executor线程池处理
- Executor线程池中的工作线程最终负责处理请求
Tomcat线程池扩展了ThreadPoolExecutor,行为稍有不同:
- 如果总线程数达到了maximumPoolSize,不会立刻抛出RejectedExecutionException,而是再次尝试将任务放入队列,如果还失败,才抛出异常
相关配置
Connector配置
配置项 | 默认值 | 说明 |
---|---|---|
acceptorThreadCount | 1 | acceptor线程数量 |
pollerThreadCount | 1 | poller线程数量 |
minSpareThreads | 10 | 核心线程数 |
maxThreads | 200 | 最大线程数 |
executor | - | Executor名称,用来引用Executor |
Executor线程配置
配置项 | 默认值 | 说明 |
---|---|---|
threadPriority | 5 | 线程优先级 |
daemon | true | 是否是守护线程 |
minSpareThreads | 25 | 核心线程数 |
maxThreads | 200 | 最大线程数 |
maxIdleTime | 60000 | 线程生存时间 |
maxQueueSize | Integer.MAX_VALUE | 队列长度 |
prestartminSpareThreads | false | 核心线程是否在服务器启动时启动 |
执行流程
Fork/Join线程池
- Fork/Join是JDK1.7加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型计算
- 所谓任务拆分,是根据分治思想,将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解,跟递归相关的一些计算
- Fork/Join在分治基础上加入了多线程,可以把每个任务分解和合并交给不同的线程来完成,进一步提升了运算效率
- Fork/Join默认创建与cpu核心数大小相同的线程池
工作窃取算法
工作窃取算法是指某个线程从其他队列里来窃取任务执行。在Fork/Join中,将一个大任务分割成若干个互不影响的子任务,为了减少线程之间的竞争,会把这些子任务分别放到不同队列里,每个队列对应一个线程来处理,当某个线程的队列中没有任务了,就可以去帮其他队列分担任务
优点
充分利用线程进行并行计算,减少了线程之间的竞争
缺点
在队列中只有一个任务时,会存在竞争,而且需要创建多个队列,消耗了更多的资源
Fork/Join的设计
- ForkJoinTask:Fork/Join线程池中的任务类,他提供在任务中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承他的子类,Fork/Join框架提供了以下两个子类:
- RecursiveAction:用于没有返回结果的任务,类比Runnable
- RecursiveTask:用于有返回结果的任务,类比Callable
- ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行,任务分割出来的子任务会添加到当前工作线程所维护的双端队列中,当一个工作线程的队列里面没有任务是,他会随机从其他工作线程的队列的尾部获取一个任务(工作窃取算法)
使用Fork/Join
使用Fork/Join线程池,完成自然序列求和:
@Slf4j
public class TestForkJoin {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
MyTask task = new MyTask(0,100);
Future<Integer> result = pool.submit(task);
try{
log.debug("{}", result.get());
}catch (Exception e){
e.printStackTrace();
}
}
}
class MyTask extends RecursiveTask<Integer>{
private int start;
private int end;
private final int THRESHOLD = 10;
public MyTask(int start,int end){
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
// 如果间隔超过了阈值则分割处理,没有超过阈值则直接处理
boolean canCompute = (start-end)<THRESHOLD;
if(canCompute){
for (int i = start; i < end; i++) {
sum += i;
}
}else{
// 取中点分割成两个子任务
int mid = (start+end)>>1;
MyTask leftTask = new MyTask(start,mid);
MyTask rightTask = new MyTask(mid+1,end);
// 两个子任务分别提交
leftTask.fork();
rightTask.fork();
// 将两个子任务汇总
sum += leftTask.join()+rightTask.join();
}
return sum;
}
}