线程池核心概述

  • Executors工厂类使用                        
  • Executors工厂类底层源码分析详解                
  • ThreadPoolExecutor自定义线程池                
  • ThreadPoolExecutor拒绝策略详解                
  • 计算机密集型与IO密集型详解            
  • 如何正确的使用线程池

线程池初步

  • 线程池,一般高并发其实是一个非常抽象的概念,要实现高并发其实不仅仅是一个JAVA 线程集合类、或者JAVA基础层面就能搞定的事情,在互联网大厂中,高并发其实涉及方方面面,从前端到后端,到支持高并发的中间组件(redis、zookeper等),最后到数据存储,持久化层面等等,都需要对高并发做一些考量和设计
  • 管理控制:首先,从管理角度就是为了更好的控制线程,使用线程池来帮助我们去管理线程,使得我们对线程的生命周期、初始化、运行状态、销毁等各个环节有一个把控
  • 系统资源:另外一点,从系统资源的角度考虑,线程池可以控制线程的数量,根据任务的多少去对线程池中的线程个数进行添加或者减少,可以回收空闲状态的线程,减少线程的频繁初始化和销毁,避免不必要的系统开销,节省系统资源,保障稳定性
  • 应用性能:从性能的角度去考虑,线程池可以配合高并发容器的设置,对任务和工作项进行缓存,异步的多线程的去处理任务,从而提高应用服务的吞吐率、消费性能,也从而提高单个线程的利用率
  • 兜底策略:从健壮性的角度去分析,线程池提供了很多拒绝策略,我们在任务过多或者处理不过来的时候,可以进行有效的拒绝策略、降级方案,以补偿的形式进行处理任务,避免因为线程池的问题对系统产生较为严重的影响

Executors

  • JDK提供了一套线程框架Executors,存储在java.util.concurrent包中,是JDK并发包的核心
  • Executors:线程工厂的角色,通过Executors可以创建特定功能的线程池

Executors创建线程池的方法

  • newFixedThreadPool()方法:该方法返回一个固定数量的线程池,该方法的线程数始终不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中等待有空闲的线程去执行
  • newSingleThreadPool ()方法:创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务列队中
  • newCachedThreadPool()方法:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若有任务,则创建线程,若无任务则不创建线程。如果没有任务则线程在60s后自动回收(空闲时间60s)
  • newScheduledThreadPool()方法:该方法返回一个SchededExecutorService对象,但该线程池可以指定线程的数量

自定义线程池ThreadPoolExecutor

  • 自定义线程池:若Executors工厂无法满足我们的需求,可以自己创建自定义线程池,其实Executors工厂类里面的创建线程方法其内部实现均是用了ThreadPoolExecutor这个类,这个类可以自定义线程。构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • 使用有界队列:在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,若大于corePoolSize,则会将任务加入队列,若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程,若线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式
  • 使用无界队列:在使用无界队列时:LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加。若后续仍有新的任务加入,而有没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存

线程池的拒绝策略

  • AbortPolicy:直接抛出异常阻止系统正常工作
  • CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务
  • DiscardOldestPolicy:丢弃最老的一个请求,尝试再次提交当前任务
  • DiscardPolicy:丢弃无法处理的任务,不给予任何处理
  • 如果需要自定义拒绝策略可以实现RejectedExecutionHandler接口
//RejectedExecutionHandler接口
public class MyRejected implements RejectedExecutionHandler{
    public MyRejected(){

    }

    @Override
    public void rejectExecution(Runnable r,ThreadPoolExecutor executor){

    }
}

如何使用好线程池

  • 线程个数大小的设置
  • 线程池相关参数配置
  • 利用Hook嵌入你的行为
  • 线程池的关闭

线程池大小设置(计算密集型/IO密集型)

  1. 计算密集型: 顾名思义就是应用需要非常多的CPU计算资源,在多核CPU时代,我们要让每一个CPU核心都参与计算,将CPU的性能充分利用起来,这样才算是没有浪费服务器配置,如果在非常好的服务器配置上还运行着单线程程序那将是多么重大的浪费。对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换。比较理想方案是: 线程数 = CPU核数+1,也可以设置成CPU核数*2,但还要看JDK的版本以及CPU配置(服务器的CPU有超线程)
  2. IO密集型: 就很好理解了,我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。那么这个线程池的数据量是不是可以随便设置呢?当然不是的,请一定要记得,线程上下文切换是有代价的。目前总结了一套公式,对于IO密集型应用: 线程数 = CPU核心数/(1-阻塞系数) 这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9。 套用公式,对于双核CPU来说,它比较理想的线程数就是20,当然这都不是绝对的,需要根据实际情况以及实际业务来调整:final int poolSize = (int)(cpuCore/(1-0.9))

线程池相关参数配置注意事项

  1. 避免线上操作数据库,查询、修改都很麻烦
  2. 使用线程池的时候都不要选择没有上限限制的配置项
  3. 不要使用没有上限的线程池和设置无界队列
  4. newCachedThreadPool的设置与无界队列的设置因为某些不可预期的情况,线程池会出现系统异常,导致线程暴增的情况或者任务队列不断膨胀,内存耗尽导致系统崩溃和异常。 我们推荐使用自定义线程池来避免该问题,这也是在使用线程池规范的首要原则
  5. 合理设置线程数量、和线程空闲回收时间,根据具体的任务执行周期和时间去设定,避免频繁的回收和创建,虽然我们使用线程池的目的是为了提升系统性能和吞吐量,但是也要考虑下系统的稳定性,不然出现不可预期问题会很麻烦
  6. 根据实际场景,选择适用于自己的拒绝策略。进行补偿,不要乱用JDK支持的自动补偿机制!尽量采用自定义的拒绝策略去进行兜底

利用Hook嵌入你的行为

  1. 利用Hook,留下线程池执行轨迹
  2. 例如ThreadPoolExecutor提供了protected类型可以被覆盖的钩子方法,允许用户在任务执行之前会执行之后做一些事情。我们可以通过它来实现比如初始化ThreadLocal、收集统计信息、如记录日志等操作。这类Hook如beforeExecute和afterExecute。另外还有一个Hook可以用来在任务被执行完的时候让用户插入逻辑,如rerminated
  3. 如果hook方法执行失败,则内部的工作线程的执行将会失败或被中断

关闭线程池

  1. 内容当线程池不在被引用并且工作线程数为0的时候,线程池将被终止。我们也可以调用shutdown来手动终止线程池。如果我们忘记调用shutdown,为了让线程资源被释放,我们还可以使用keepAliveTime和allowCoreThreadTimeOut来达到目的
  2. 当然,稳妥的方式是使用虚拟机Runtime.getRuntime().addShutdownHook方法,手工去调用线程池的关闭方法

相关代码

package com.bfxy.thread.core.pool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


public class UseThreadPoolExecutor {

	
	public static void main(String[] args) {
		
		ThreadPoolExecutor pool = new ThreadPoolExecutor(1,	// corePoolSize: 核心线程数,线程池初始化的时候就会被创建
				3,	// maximumPoolSize: 线程池的最大上限	//在使用无界队列的时候, 此参数 不起作用
				60,	//线程的存活时间
				TimeUnit.SECONDS,
				//workQueue:BlockingQueue接口下面的实现类
				//new ArrayBlockingQueue<>(2),	//使用有界队列: ArrayBlockingQueue
				new LinkedBlockingQueue<>(),	//使用无界队列: LinkedBlockingQueue
				new ThreadFactory() {	//threadFactory 线程工厂, 用于获取一个新的线程, 然后把该线程 投递到我们的线程池中去
					@Override
					public Thread newThread(Runnable r) {
						Thread th = new Thread(r, "order-thread");
						if(th.getPriority() != Thread.NORM_PRIORITY) {
							th.setPriority(Thread.NORM_PRIORITY);
						}
						if(th.isDaemon()) {
							th.setDaemon(false);
						}
						return th;
					}
				},	
				
				//使用无界队列时, 拒绝策略不起到作用
				new RejectedExecutionHandler() {
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
						System.err.println("当前的任务已经被拒绝: " + r.toString());
					}
				});
		
		Task t1 = new Task(1);
		Task t2 = new Task(2);
		Task t3 = new Task(3);
		Task t4 = new Task(4);
		Task t5 = new Task(5);
		Task t6 = new Task(6);
		
		/**
		//线程池提交任务的方法:
		pool.execute(t1);  		//execute: 如果你的任务没有返回值, 则使用该方法提交任务
		pool.submit(t1);		//submit: 如果你的任务有返回值, 则使用该方法提交任务, 返回一个Future对象(Future模式)
		*/
		
		/**
		 * 
		 * 在使用有界队列时:
		 * 1 若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程
		 * 2 若大于corePoolSize,则会将任务加入队列
		 * 3 若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程
		 * 4 若线程数大于maximumPoolSize,则执行拒绝策略。
		 */
		
		// 1 若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程
		pool.execute(t1);	//core size = 1  t1任务会被核心线程执行
		// 2 若大于corePoolSize,则会将任务加入队列
		pool.execute(t2);	// 有界队列容量为: 2
		pool.execute(t3);
		// 3 若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程, 并执行该任务
		pool.execute(t4);	// 线程池中的总线程数 2  , maximumPoolSize = 3 
		pool.execute(t5);	// 线程池中的总线程数 3  , maximumPoolSize = 3 
		// 4 若线程数大于maximumPoolSize,则执行拒绝策略。
		pool.execute(t6);
		
		
		pool.shutdown();
		
	}
}