线程池

线程学习:https://blog.csdn.net/qq_42082278/article/details/107465102

1、线程池的实现原理

下图所示为线程池的实现原理:调用方不断地向线程池中提交任务;线程池中有一组线程,不断从队列中取任务,这是一个典型的生产者-消费者模型.
在这里插入图片描述
要实现这样一个线程池,有几个问题需要考虑:

  1. 队列设置多长?如果是无界的,调用方不断地往队列中放任务,可能导致内存耗尽。如果是有界的,当队列满了后,调用方如何处理
  2. 线程池中的线程个数是固定的,还是动态变化的?
  3. 每次提交新任务,是放入队列?还是开新线程?
  4. 当没有任务的时候,线程时睡眠一段时间 ?还是进入阻塞?如果进入阻塞,如何唤醒?
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在其构造方法中提供了几个核心配置参数,来配置不同的策略的线程池。
在这里插入图片描述
上面的参数解释

  1. corePoolSize:在线程池中始终维护的线程个数。
  2. maxPoolSize:在corePoolSize已满,队列也满的情况下,扩充线程至此值。
  3. keepAliveTime/TimeUnit:maxPoolSize中的空闲线程,销毁所需要的时间,总线程数收缩回corePoolSize.
  4. blockingQueue:线程池所用的队列类型。
  5. threadFactory:线程创建工厂,可以自定义,有默认值(Executors.defaultThreadFactory() ).
  6. 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()的区别
  1. shutdown()不会清空任务队列,会等所有任务执行完成,shutdownNow()清空任务队列。
  2. 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实现了按时间调度来执行任务:

  1. 延迟执行任务
    在这里插入图片描述

  2. 周期执行任务
    在这里插入图片描述
    区别如下:

AtFixedRate:按固定频率执行,与任务本身执行时间无关。但有个前提条件,任务执行时间必须小于间隔时间,例如间隔时间是5s,每5s执行一次任务,任务的执行时间必须小于5s。

WithFixedDelay:按固定间隔执行,与任务本身执行时间有关。例如,任务本身执行时间是10s,间隔2s,则下一次开始执行的时间就是12s。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值