java基础巩固-宇宙第一AiYWM:为了维持生计,多高(多线程与高并发)_Part7~整起(打手集团【线程池】)

特此感谢,低并发编程老师的公众号里面讲的线程池一节,受益匪浅,才有了自己整理的笔记

  • 先说,为啥要用线程池,之前咱们也搞过数据库连接池,为啥要有这些池化技术…
    • 线程池分工要明确,单一职责哦
    • 创建多少线程池合适
      在这里插入图片描述
    • 线程池主要解决两个问题:【使用线程池的好处:】
      • 是当执行大量异步任务时线程池能够提供较好的性能。在不使用线程池时,每当需要执行异步任务直接 new 个线程来运行,而线程的创建和销毁是需要开销的。而线程池里面的线程是可复用的不需要每次执行异步任务时都重新创建和销毁线程
        • 相当于 降低资源消耗, 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
          • 线程池是怎么复用线程的----ThreadPoolExecutor中有个内置对象Worker,每个worker都是一个线程,worker线程数量和参数有关,每个worker会while死循环从阻塞队列中取数据,通过置换worker中Runnable对象,运行其run方法起到线程置换的效果,这样做的好处是避免多线程频繁线程切换,提高程序运行性能。
        • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行
        • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行 统一的分配,调优和监控。
      • 二是线程池也提供了一种资源限制和管理的手段【线程池提供了一种限制和管理资源(包括执行一个任务)的方式】,比如可以限制线程的个数,动态新增线程等。每个ThreadPoolExecutor也保留了一些基本的统计数据, 比如当前线程池完成的任务数目等。
        • 线程池也提供了许多可调参数和可扩展性接口 ,以满足不同情景的需要 ,开发时可以使用更方便的Executors的工厂方法, 比如 newCachedThreadPool (线程池线程个数最多可达Integer.MAX_VALUE ,线程自动回收)、 newFixedThreadPool (固定大小的线程池)、newSingleThreadExecutor (单个线程)等来创建线程池,当然用户还可以自定义

然后,咱们先看一下线程池这里比较重要的一个关系图:
在这里插入图片描述

  • java.util.concurrent.ScheduledThreadPoolExecutor的原理,用时到Java并发编程之美中自取 。这是一个可以在指定一定延迟时间后或者定时进行任务调度执行的线程池。
    • 其内部使用DelayQueue来存放具体任务。任务分为三种
      • 其中一次性执行任务执行完毕就结束了
        • 执行任务需要实现的 Runnable 接口 或 Callable接口。或者说Runnable 接口或 Callable 接口 实现类都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行
      • fixed-delay任务保证同一个任务在多次执行之间间隔固定时间
      • fixed-rate 任务保证按照固定的频率执行,任务类型使用period的值来区分
        在这里插入图片描述
        在这里插入图片描述

PART1:然后,如果让咱们自己设计一个线程池,咱们该如何考虑呢?(再次感谢低并发编程老师)
在这里插入图片描述
一步一步拆解框图,如下:

  • 如果我自己来设计这个工具类的话(出题人给定了一个接口:public interface Executor{public void execute(Runnable r)};)。那思路就是,我写一个类实现出题人给的接口,类里面重写接口里面的方法,方法体就是最初的想要异步执行的程序,new Thread®.start();相当于不管是伙伴A,伙伴B…都来调用我这个子类中的重写过的方法就行,不用你们自己去new Thread®.start();
    public ExecutorVersion01 implements Executor{
    	public void execute(Runnable r){
    		new Thread(r).start();
    	}
    }
    
    • Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题【this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误】
  • 方便了一点点,但是,要是一亿个人都调这个子类中的方法去想要异步执行,他其实也是创建了一亿个线程呀,这么多线程肯定不合适吧,你一个饭馆会招聘这么多服务员嘛,肯定不会呀,肯定要共用起来好一点呀。好,那先实现一下控制一下线程的数量
    在这里插入图片描述
  • 一个Worker貌似有点少(要是tasks队列满了怎么办,相当于客人来的很多你一个服务员手脚再麻利也忙不过来呀)。好,那先把Worker线程的数量增加一下
    • 具体数量让使用者决定。调用时再传入(叫做核心线程数corePoolSize)
      在这里插入图片描述
  • 关于其中的execute()方法【常用的提交任务的方法,也就是使用 executor.execute(worker)来提交一个任务到线程池中去
    在这里插入图片描述

🕴执行execute()方法和submit()方法的区别是什么

  • execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
    在这里插入图片描述
    在这里插入图片描述
  • submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。【使用 get(long timeout,TimeUnit unit)方法的话,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException】
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    • public interface ExecutorService中也有和execute()方法功能类似的一个方法:
      • Future submit(Callable task):提交值返回任务以执行,并返回代表任务待处理结果的Future。 当我们把 Runnable接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象
        • Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果
        • 实现 Runnable 接口和 Callable 接口的区别:
          • Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例Runnable 接口 不会返回结果或抛出检查异常,但是 Callable 接口 可以返回结果或抛出检查异常。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口 ,这样代码看起来会更加简洁。工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object result))
            在这里插入图片描述
      • Future<?> submit(Runnable task):提交一个可运行的任务执行,并返回一个表示该任务的未来。
      • Future submit(Runnable task, T result) :提交一个可运行的任务执行,并返回一个表示该任务的未来。

至此,设计思路大体完成,打眼一看,还有几个缺陷:

  • 假设当提交任务的数量突增时,工作线程Worker和任务队列Tasks都被占满了,就只能走拒绝策略,其实就是被丢弃掉,这还不是赶走客人嘛
    在这里插入图片描述
    在这里插入图片描述

我们可以发明一个新属性,叫最大线程数maximumPoolSize.当核心线程数和队列都满了时,新提交的任务仍然可以通过创建新的工作线程(叫做非核心线程),直到工作线程数达到maximumPoolSize为止(这样就可以缓解一时的QPS高峰期了,我也不用无脑去设置过大的核心线程数了)

  • 开始时当workCount < corePoolSize时,通过创建新的Worker(此时虽然前几个线程本质上和之前的Worker一样,但是此时叫做核心线程)来执行任务
  • 当workCount >= corePoolSize时,停止创建新线程,把任务直接丢到任务队列中
  • 又当任务队列一满时,workCount < maximumPoolSize时,不再直接走拒绝策略(满了就丢那种),而是创建非核心线程,直到workCount = maximumPoolSize再满了的话再走拒绝策略
    在这里插入图片描述
    简单设计完毕~再次感谢低并发编程老师的文章。

PART2:在重新回顾一下上述过程,线程池的定义
线程池(程序运行的本质就是(进程和线程)占用系统的资源,所以为了优化资源的使用,才有了池化技术),在池子里创建线程,谁要用线程你谁到池子里来拿就行。
在这里插入图片描述
接下来就是:三大方法(也可以说是 常用的JAVA线程池三种类型Executors这个工具类中的三大方法)+7大参数+4种拒绝策略

  • 三大方法 (Executors这个工具类中的三大方法,但是阿里巴巴开发手册上说 不允许用Executors去创建线程池而是通过ThreadPoolExecutor的方式,,因为ThreadPoolExecutor这样的处理方式让咱们更加明确线程池的运行规则,规避资源耗尽的风险):
    • Executors 返回线程池对象的弊端如下:
      • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM
      • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM
    • (这三大方法都是Executors工具类的,咱们再看看这个工具类:顶层工具类Executors详解(可以把这个Executors类看作是一个工具类,它里面有三大方法可以用来创建出三种类型的线程池,也就是创建常用的JAVA线程池三种类型,)
      在这里插入图片描述
      在这里插入图片描述
      框中的三个用来创建线程池方法,如下:
      在这里插入图片描述
      • 当然啦,除了Executors类中的三大方法可以用来创建出三种类型的线程池,也可以通过ThreadPoolExecutor构造方法实现创建线程池
        在这里插入图片描述
        • Executors类中的三大方法内部其实也是调用了ThreadPoolExecutor构造方法
          在这里插入图片描述
        • 阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池
          在这里插入图片描述
    • public static ExecutorService newCachedThreadPool();public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
      在这里插入图片描述
      整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。适合任务数比较密集,但每个任务执行时间较短的情况
      在这里插入图片描述
      • 创建一个可缓存线程池【可根据实际情况调整线程数量的线程池】,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
        • 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用
          在这里插入图片描述
      • 创建一个按需创建线程的线程池,初始线程或者说 CachedThreadPool 的corePoolSize个数为0,最多线程maximumPoolSize个数为 Integer. MAX_VALUE【可能会创建大量线程,从而导致 OOM】,并且阻塞队列为同步队列.keeyAliveTime=60说明只要当前线程在60s内空闲则回收这个类型的特殊之处在于加入同步队列的任务会被马上执行,同步队列里面最多只有一个任务
      • 这种类型的线程池特点是:工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程.如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。.在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。
    • public static ExecutorService newFixedThreadPool(int nThreads);public static ExecutorService newFixedThreadPool(int nThreads,ThreadFactory threadFactory)
      在这里插入图片描述
      • 创建一个指定工作线程数量的线程池【固定线程数量的线程池】。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源.
        • 该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若线程池中没有空闲线程,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务
          在这里插入图片描述
          在这里插入图片描述
        • 不推荐使用FixedThreadPool
          • FixedThreadPool 使用**无界队列 LinkedBlockingQueue**(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
            • 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize
            • 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值
            • maximumPoolSize 将是一个无效参数,那么肯定使用无界队列时 keepAliveTime 也将是一个无效参数
            • 运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)
    • public static ExecutorService newSingleThreadExecutor();public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory);
      在这里插入图片描述
      • 创建一个单线程化【只有一个线程的线程池】的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
        • 若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
          在这里插入图片描述
      • 不推荐使用SingleThreadExecutor
        • SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM
    • newScheduleThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行
      • 比如让每周四18:00:00定时执行任务
        在这里插入图片描述
  • 三大方法的底层都是在方法中return new ThreadPoolExecutor(.....)来实现开启了线程池,所以总的来说,最后创建线程池的还是得靠人家ThreadPoolExecutor 类中提供的四个构造方法
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    调用Executors的三大方法返回或者说创建一个线程池的代码如下:
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Executors.newFixedThreadPool(5);//创建一个有固定数目线程的线程池
Executors.newCachedThreadPool();//创建一个可伸缩的,遇强则强遇弱则弱的线程池

在这里插入图片描述

那咱们挖一挖这个ThreadPoolExecutor里面都有啥子嘞?【ThreadPoolExecutor 类中提供的四个构造方法中三个都是在这个最长的构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)
在这里插入图片描述
在这里插入图片描述

  • 🕴shutdown() VS shutdownNow()
    在这里插入图片描述
    • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕
    • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 ListshutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止
      在这里插入图片描述
  • isTerminated() VS isShutdown()
    在这里插入图片描述
    • isShutDown 当调用 shutdown() 方法后返回为 true。
    • isTerminated 当调用 shutdown() 方法后,并且要等所有提交的任务完成后返回为 true

有几个注意点:
在这里插入图片描述
在这里插入图片描述
也就是说Executors工具类中的三大方法底层都是在方法中return new ThreadPoolExecutor(.....)来实现开启了线程池。ThreadPoolExecutor 类中提供的四个构造方法中三个都是在这个最长的构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)

  • 线程池巧妙地使用Integer类型的原子变量来记录线程池状态和线程池中的线程个数。通过线程池状态来控制任务的执行,每个Worker线程可以处理多个任务。线程池通过线程的复用减少了线程创建和销毁的开销。
    public class ThreadPoolExecutor extends AbstractExecutorService {
    	/**
    	* 成员变量ctl是一个Integer的原子变量,用来记录线程池状态和线程池中线程个数,类似于ReeentrantReadWriteLock使用一个变量来保存两种信息。这里假设Integer类型是32位二进制表示,则其中高位用来表示线程池状态,后面29位用来记录线程池线程个数
    	*/
    	private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    	/**
    	* 线程个数掩码位数,并不是所有平台的int类型都是32位的,所以准确地说,是具体平台Integer的二进制位数-3后的剩余位数所表示的数才是线程的个数
    	*/
    	private static final int COUNT_BITS = Integer.SIZE - 3;
    	//线程最大个数(低29位)00011111111111111111111111111111
    	private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    
    	//(高3位):11100000000000000000000000000000
    	private static final int RUNNING    = -1 << COUNT_BITS;
    	//(高3位):00000000000000000000000000000000
        private static final int SHUTDOWN   =  0 << COUNT_BITS;
        //(高3位):00100000000000000000000000000000
        private static final int STOP       =  1 << COUNT_BITS;
        //(高3位):01000000000000000000000000000000
        private static final int TIDYING    =  2 << COUNT_BITS;
        //(高3位):01100000000000000000000000000000
        private static final int TERMINATED =  3 << COUNT_BITS;
    
    	//获取高3位(运行状态)
    	private static int runStateOf(int c){ 
    		return c & ~CAPACITY; 
    	}
    	//获取低29位(线程个数)
        private static int workerCountOf(int c){
        	return c & CAPACITY; 
        }
        //计算ctl新值(线程状态与线程个数)
        private static int ctlOf(int rs, int wc){ 
        	return rs | wc; 
        }
    }
    
    • 线程池中的CTL属性:状态容量
      在这里插入图片描述
      • ctl是线程池中一个属性,本质就是int类型的数值。高三位描述线程池的状态,低29位描述线程池中现存的工作线程的数量。因为线程池在执行任务时需要多次判断线程池状态,从而来确定任务是否需要执行或者说任务以那种方式执行

ThreadPoolExecutor里面的一些方法,分析如下:
在这里插入图片描述

// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static int workerCountOf(int c) {
    return c & CAPACITY;
}

private final BlockingQueue<Runnable> workQueue;

/**
* execute()方法的作用是提交任务(用户线程提交的任务)command到线程池进行执行。
*/
public void execute(Runnable command) {
	//如果任务为null抛出NullPointerException
	if (command == null){
         throw new NullPointerException();
    }
    //)ctl 中保存的线程池当前的一些状态信息,ctl.get()获取当前线程池的状态+线程个数变量的组合值
    int c = ctl.get();
    /**
    * 下面会涉及到3步操作
    */
    //1.首先判断当前线程池中线程个数是否小于corePoolSize,如果小于则开启新线程运行或者说通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中(也就是说会向workers里面新增一个核心线程(core线程)执行该任务),然后,启动该线程从而执行任务。
    if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
    }
	
	//2.如果当前线程池中线程【当前执行的任务数量】个数大于等于corePoolSize则向下执行代码
	//通过 isRunning 方法判断线程池状态,如果线程池处于RUNNING状态并且队列可以加入任务,则添加当前任务到阻塞队列。这里需要判断线程池状态是因为有可能线程池己经处于非 RUNNING状态 而在非RUNNING状态下是要抛弃新任务的
	if (isRunning(c) && workQueue.offer(command)) {
			//二次检查。要二次检查的原因是添加任务到任务队列后执行reject(command);前可能线程池的状态已经变化了
            int recheck = ctl.get();
            //再次获取线程池状态,如果当前线程池状态状态不是RUNNING则从队列中删除任务,并尝试判断线程是否全部执行完毕,移除后并执行拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //否则说明二次检查通过,则重新判断当前线程池中是否还有线程,如果当前线程池为空则添加一个线程或者说新创建一个线程并执行。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);//新创建一个线程并执行。
            //如果任务队列满则新增线程如图上的thread3和thread4来执行任务.如果当前线程池中线程个数>maximumPoolSize,则新增失败则执行拒绝策略
            //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容
    }else if (!addWorker(command, false)){
            reject(command);
    }   
}

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        //第一部分双重循环的目的是通过CAS操作增加线程数:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            //
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;//这个if是当这三种情况出现时会return false:当前线程池状态为STOP\TIDYING\TERMINATED;当前线程池状态为SHUTDOWN并且己经有了第一个任务;当前线程池状态为SHUTDOWN并且任务队列为空。
			//循环CAS增加线程个数
            for (;;) {
                int wc = workerCountOf(c);
                //如果线程个数超过限制则返回false
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //CAS增加线程个数,同时只有一个线程成功
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                //CAS失败了,则看线程池状态是否变化了,如果变化了则跳到外层循环重新尝试获取线程池状态,否则内层循环重新CAS
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

		//第二部分主要是把并发安全的任务加到workers里面并且启动任务执行
		//执行到这里说明CAS成功了
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
        	//创建Worker,一个工作线程
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
            	
                final ReentrantLock mainLock = this.mainLock;
                //加独占锁。为了实现workers同步,因为可能多个线程调用了线程池的execute()方法
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    //重新检查线程池状态,以避免在获取锁前调用了 shutdown接口。如果线程池己经被关闭,则释放锁,新增线程失败
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                            //添加任务。使用全局的独占锁来控制把新增的Worker添加到工 workers中
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                //添加任务成功(新增工作线程成功)后则启动任务
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

如上面图,用户线程提交任务到线程池后由Worker来执行

Worker(Runnable firstTask) {
//在构造函数内 首先设 Worker的状态为1,这是为了避免当前Worker在调用rnnWorker方法前被中断(当其他线程调用了线程池的shutdownNow时,如果Worker状态>=0则会中断该线程)。这里设置了线程的状态为-1,所以该线程就不会被中断了
            setState(-1); // 在调用runWorker前禁止中断
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);//创建一个线程
}
  • 工作线程存储在线程池的workers中,workers
  • java中的工作线程就值得是Worker线程,
    addWorker(Runnable, true/false) //addWorker添加一个Worker对象到线程池中,Runnable这个位置不管用什么变量表示就是指具体要执行的任务,true代表核心线程数、false表示最大线程数
    
    • Worker其实就是线程池中的一个内部类,这个内部类继承了AQS【而Worker继承了AQS的目的就是为了添加标识来判断当前工作线程是否可以被打断】,实现了Runnable
      • 最大线程和核心线程都是在说Worker
    • 线程池执行任务,就是执行Runnable那个位置传进来的东西时实际上就是调用了Worker类中的run方法内部的runWorker方法
  • 调用shutdown方法后线程池也就不会接受新的任务了,但是工作队列中的任务还是要执行的,该方法会立即返回并不等待队列任务完成后再返回
  • 调用shutdownNow 方法后线程池就不会接受新的任务了,并且会丢弃作队列里面的任务,正在执行的任务会被中断,该方法会立刻返回 ,并不等待激活的任务执行完成。返回值为这时候队列里面被丢弃的任务列表。
  • 当线程调 awaitTermination 方法后,当前线程会被阻塞,直到线程池状态变为TERMINATED 才返回,或者等待时间超时才返回。

ThreadPoolExecutor里面的线程池状态:ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量
在这里插入图片描述

  • RUNNING:接受新任务并且处理阻塞队列里的任务
  • SHUTDOWN :拒绝新任务但是处理线程池内部的阻塞队列里现有的任务【人家已经在池子中了,你不能说把人家赶走吧】
  • STOP :拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务。
  • TIDYING:过渡状态,会从SHUTDOWN和STOP转到TIDYING状态
  • TERMINATED:当线程池达到了TIDYING之后,源码中会自动调用TERMINATED,进入了TERMINATE状态
    在这里插入图片描述
    在这里插入图片描述

再看看ThreadPoolExecutor里面线程池中的常用七大参数:
在这里插入图片描述
内部有一个this进行调用,咱顺着this往下找
在这里插入图片描述
在这里插入图片描述
那咱们再继续顺着看看这七个参数:

public ThreadPoolExecutor(
        int corePoolSize,//核心线程数目(最多保留的线程数)
        int maximumPoolSize,//最大线程数目
        long keepAliveTime,//生存时间-针对救急线程
        TimeUnit unit,//时间单位-针对救急线程
        BlockingQueue<Runnable> workQueue,//阻塞队列
        ThreadFactory threadFactory,//线程工厂-可以为线程创建时起个好名字
        RejectedExecutionHandler handler) //拒绝策略
{
    ... // 省略一些参数校验
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下来一个一个具体看:

  • int corePoolSize:核心线程数(核心线程池大小):线程池一直运行,核心线程就不会停止。最小可以同时运行的线程数量
    在这里插入图片描述
    • 如何确定项目中需要的线程池大小:
      • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
        • CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上
        • 线程数更严谨的计算的方法应该是:最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间)),其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)
      • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
  • int maximumPoolSize:最大线程池大小【当workQueue队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数maximumPoolSize】。非核心线程数量=maximumPoolSize-corePoolSize
    在这里插入图片描述
  • long keepAliveTime:非核心线程的空闲时间。如果非核心线程在keepAliveTime内没有运行任务,非核心线程会消亡
    在这里插入图片描述
    • 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁
  • TimeUnit util:空闲时间keepAliveTime参数的时间单位(超时单位)
  • BlockingQueue workQueue:用于保存等待执行的阻塞队列(线程安全的阻塞队列)【当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数corePoolSize,如果达到的话,新任务就会被存放在workQueue队列中】。线程安全的阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayedWorkQueue等,用来存放线程任务
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    • ArrayBlockingQueue:基于数组的有界的
    • LinkedBlockingQueue 对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是基于链表的无界队列。由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务
      • 这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程
    • SynchronousQueue(最多只有一个元素的同步队列) 第 二种阻塞队列是 SynchronousQueue,对应的线程池是 CachedThreadPool。 线程池 CachedThreadPool 的最大线程数是 Integer 的最大值,可以理解为线程数是可以无限扩展的。CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。 我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况
    • DelayedWorkQueue 第三种阻塞队列是DelayedWorkQueue,它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor这两种线程池的最大特点就是可以延迟执行任务比如说一定时间后执行任务或是每隔一定的时间执行一次任务
      • DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行
      • SingleThreadScheduledExecutor:延时/定时执行,替换了java.util.Timer
  • ThreadFactory threadFactory:线程工厂,用来创建线程的,一般不用动(Executors.defaultThreadFactory();)
    在这里插入图片描述
    • 线程池中的源码,或者说看看线程池是怎么实现线程的?
      在这里插入图片描述
  • RejectedExecutionHandler handler:拒绝策略(有四种拒绝策略,通过实现RejectedExecutionHandler接口,饱和策略,各有各的拒绝特点)。当队列workQueue满并且线程个数达到 maximunPoolSize后采取的策略就是饱和策略或者叫拒绝策略【ThreadPoolTaskExecutor 定义一些策略:】, 比如AbortPolicy (抛出异常)、CallerRunsPolicy(使用调用者所在线程来运行任务)、DiscardOldestPolicy(调用 poll丢弃一个任务,执行当前任务)以及DiscardPolicy (默默丢弃,不抛出异常)
    • ThreadPoolExecutor.AbortPolicy: 抛出 RejectedExecutionException来拒绝新任务的处理
      • Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列
    • ThreadPoolExecutor.CallerRunsPolicy: 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,可以选择这个策略
    • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉新任务
    • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将 丢弃最早的未处理的任务 请求。

在这里插入图片描述
拒绝策略(有四种拒绝策略,各有各的拒绝特点);相当于银行那种,有五个柜台,然后假设还有一个候客区共有5个座位。然后假如人很多时,柜台已经有五个人在办业务了并且候客区也坐满了五个人,那么如果第十一个来了,就得用拒绝策略把第十一个人拒绝了
在这里插入图片描述

  1. 第一种拒绝策略是:(在线程池这里体现出来就是如果第11个人来了就不处理并抛出异常):AbortPolicy : 线程任务丢弃报错。默认饱和策略
    在这里插入图片描述
  2. CallerRunsPolicy :线程池之外的线程直接调用run方法执行List item
  3. 队列满了后,第11个人就会被默默抛弃不会被处理(执行),也不会抛出啥异常。DiscardPolicy : 线程任务直接丢弃不报错
    在这里插入图片描述
  4. DiscardOldestPolicy : 将workQueue队首任务丢弃,将最新线程任务重新加入队列执行List item

那咱们如何合理配置线程池参数:自定义线程池就需要我们自己配置最大线程数 maximumPoolSize ,为了高效的并发运行,这时需要看我们的业务是IO密集型还是CPU密集型。

  • CPU密集型 CPU密集的意思是该任务需要最大的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程)。而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那么多。
  • IO密集型 IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上这种加速主要就是利用了被浪费掉的阻塞时间。IO 密集型时,大部分线程都阻塞,故需要多配制线程数。公式为:CPU核数*2CPU核数/(1-阻塞系数)。阻塞系数在0.8~0.9之间。查看CPU核数:System.out.println(Runtime.getRuntime().availableProcessors());

下来再看看几个赠送的礼品:

  • 线程池执行任务的流程
    在这里插入图片描述
    • 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象,然后线程池执行execute/submit方法向线程池添加任务【使用 executor.execute(worker)来提交一个任务到线程池中去】,当任务小于核心线程数corePoolSize,如果再来新任务的话,线程池中可以创建新的线程来执行任务。
      • 把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable task))。
      • 如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象(submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
      • 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行
    • 当任务大于核心线程数corePoolSize,就向阻塞队列LinkedBlockingQueue添加任务
    • 如果阻塞队列已满,需要通过比较参数maximumPoolSize,在线程池创建新的线程,当线程数量大于maximumPoolSize,说明当前设置线程池中线程已经处理不了了,就会执行饱和策略
      在这里插入图片描述
      使用线程池时:
      在这里插入图片描述
      Executor和Executors的区别:
  • Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求
  • Executor 接口对象能执行我们的线程任务。ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。使用ThreadPoolExecutor 可以创建自定义线程池。Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果。
    在这里插入图片描述
    在这里插入图片描述
  • Tomcat线程池:
    在这里插入图片描述
    • 如果总线程数达到maximumPoolSize
      • 这时不会立刻抛RejectedExecutionException异常
      • 而是再次尝试将任务放入队列,如果还失败,才抛出RejectedExecutionException异常
        在这里插入图片描述
        配置文件中的各个参数如下:
        在这里插入图片描述
        在这里插入图片描述
  • 监测线程池运行状态:
    • SpringBoot 中的 Actuator 组件

ok,拜拜~

巨人的肩膀:
低并发编程
Java并发编程之美
狂神说视频

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值