java 线程池 Executors 核心代码 原理详解 jdk 1.8

一、前言

前段时间在公司分享过两次java线程池的实现原理,但是貌似大家理解的不是很深入,在应用的时候发现被培训的人并没有抓住核心点,并不理解线程池的核心原理,所以再完整的梳理一遍源码,希望可以帮助大家理解线程池的核心逻辑。本篇先仅讲解一下Executors创建线程池的示例及适用场景,线程池的原理浅析请参考我的另一篇文章https://blog.csdn.net/leandzgc/article/details/103111658

二、线程池的使用

2.1、jdk自带创建线程池的几种方法

固定大小线程池  Executors.newFixedThreadPool

单线程线程池 Executors.newSingleThreadExecutor

缓存线程池 Executors.newCachedThreadPool

定时线程池 Executors.newScheduledThreadPool

偷窃线程池 Executors.newWorkStealingPool(jdk1.8新增,还没仔细研究,不深入展开讲解)

原生线程池 new ThreadPoolExecutor()


其实通过ExecutorService创建的前4种线程池是不推荐使用的,这一规范在阿里开发手册中也可以找到。原因是因为:他们的底层用的也是ThreadPoolExecutor,且特殊情况下默认参数会导致程序异常,非常不稳定。所以除非全程逻辑可控,否则请使用原生的ThreadPoolExecutor来创建多线程。

2.2、Executors.newFixedThreadPool

适用场景:定长线程池,适合任务量并发及执行耗时相对平稳的场景(生产和消费速度相对平稳且对等,仅平稳但不对等也不适合直接使用该方法)。

用法示例:

package com.thunisoft.test.ThreadPoolTest;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;

public class ThreadPoolTest {

	private static int N_NUM_10 = 10;

	public static void main(String[] args) {
		// TODO Auto-generated method stub

	}

	public void testFixedThreadPool() {
		/**
		 * 业务场景假设:该应用为后台应用,主要用于定期检索消息队列中待发送短信,并把未发送的短信通过接口发送给短信平台。
		 * 同时因短信平台方处理效率有限,只支持单应用10个绝对并发,强制要求调用方限制调用连接数。
		 */
		//1、创建线程池--因为要限制最大活跃线程数数量,所以可以选择使用固定线程池,保证活跃线程数不会超过阈值(不考虑各独立线程处理各自未与短信平台交互时的相对并发,那样例子写起来就复杂了)
		//1.1、可使用最简单的方法
		ExecutorService fixedThreadPool = Executors.newFixedThreadPool(N_NUM_10);

		//1.2、还可以指定生成的线程名称,自定义一个线程工厂即可
//		ExecutorService fixedThreadPoolCustomThreadName = Executors.newFixedThreadPool(N_NUM_10,
//				// 这里可以在实例化线程时传递自己的参数,例如给每个业务组的线程都增加一个前缀,在运行时很容易区分出来所属线程组
//				new BasicThreadFactory.Builder().namingPattern("sendMsg-%d").build());

		//2、使用线程池
		// 省略从队列中获取数据的步骤,这里应该触发很多次submit方法
		fixedThreadPool.submit(new Runnable() {
			// 使用匿名内部类来创建线程
			public void run() {
				// 这里直接从队列里面获取需要发送的短信,并根据返回结果放入发送成功的队列或者放入发送失败的队列,逻辑直接结束
				// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
			}
		});
	}

}

核心参数解析:

int nThreads:线程数大小,用于控制线程池的核心线程数及最大线程数。为啥就一个参数?对,没错,通过该方法创建的线程池核心线程数及最大线程数是一样的。

ThreadFactory threadFactory(可选):线程池创建线程时的构建工厂,可以在线程池创建线程时改变线程实例的默认属性(可以看一下Thread的构造函数)。

其实该方法的内部是帮我们实例化了一个ThreadPoolExecutor,并保证核心线程数与最大线程数一致,空闲存活时间为0,构建了一个默认的队列。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }

// ...
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue(),
                                      threadFactory);
    }

注意事项:若无法保证任务生产逻辑的任务量是可控的,请不要使用该方法创建线程池,否则可能会出现构建过多的待处理任务实例被放入默认队列中,而队列构建时未给最大值,可存放Integer.MAX_VALUE个对象,引发OOM。

2.3、Executors.newSingleThreadExecutor

适用场景:单个活跃线程线程池,适合长期固定循环执行的场景(不一定是相同的线程任务对象,只要这一组任务需要统一管理,且还不想不可控的创建N多线程来浪费系统资源,就可以使用该方法创建线程池)。

用法示例:

package com.thunisoft.test.ThreadPoolTest;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;

public class ThreadPoolTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

	}

	public void testSingleThreadPool() {
		/**
		 * 业务场景假设:该应用为后台应用,主要用于分批从消息队列中获取数据,并解析并持久化到指定的数据库表中。
		 * 业务数据的类型不固定,只是遵守了同一组协议,所以任务的处理对象可能是不同类型的(例如有json的,有xml的,有需要调用httpApi二次获取的等)
		 */
		//1、创建线程池--其实适用场景跟new Thread差不多,但是线程池具有崩溃自动恢复机制,而且人家还有个队列来管理任务,这些是直接实例化Thread比不了的
		//1.1、可使用最简单的方法
		ExecutorService signleThreadPool = Executors.newSingleThreadExecutor();

		//1.2、还可以指定生成的线程名称,自定义一个线程工厂即可
//		ExecutorService signleThreadPoolCustomThreadName = Executors.newSingleThreadExecutor(
//				// 这里可以在实例化线程时传递自己的参数,例如给每个业务组的线程都增加一个前缀,在运行时很容易区分出来所属线程组
//				new BasicThreadFactory.Builder().namingPattern("td-%d").build());

		//2、使用线程池
		// 省略从队列中获取数据的步骤,这里应该触发很多次submit方法
		signleThreadPool.submit(new Runnable() {
			// 使用匿名内部类来创建线程
			public void run() {
				// 这里直接从队列里面获取需要处理的数据,清洗后持久化到数据库,逻辑直接结束
				// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
			}
		});
	}

}

核心参数解析:

ThreadFactory threadFactory(可选):线程池创建线程时的构建工厂,可以在线程池创建线程时改变线程实例的默认属性(可以看一下Thread的构造函数)。

其实该方法的内部是帮我们实例化了一个ThreadPoolExecutor,并保证核心线程数与最大线程数都是1,空闲存活时间为0,构建了一个默认的队列。

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

// ...
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue(),
                                    threadFactory));
    }

注意事项:若无法保证任务生产逻辑的任务量是可控的,请不要使用该方法创建线程池,否则可能会出现构建过多的待处理任务实例被放入默认队列中,而队列构建时未给最大值,可存放Integer.MAX_VALUE个对象,引发OOM。

2.4、Executors.newCachedThreadPool

适用场景:缓存线程池,适合任务量并发相对稳定且执行耗时较短的场景(因为该方法创建的线程池没有核心线程数,且线程的空闲活跃时间为60秒。如果并发和执行耗时波动都很大,个人感觉用这个方法也不太好,因为线程池同样会频繁的创建和销毁线程对象)。

用法示例:

package com.thunisoft.test.ThreadPoolTest;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;

public class ThreadPoolTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

	}

	public void testCachedThreadPool() {
		/**
		 * 业务场景假设:该应用为后台应用,主要用于将订单状态同步到数据库中。
		 */
		//1、创建线程池--通过主键更新速度本身很快(毫秒级,到达秒级就要确认索引或分库分表是否合理了),且订单业务本身在某一时间段内是相对稳定的
		// (没找到数据支撑,一般应该是在中午休息时间或下班时间及各活动时间订单量会飙升,其他时间段始终处于一个相对稳定的水平)
		// 这时候就特别适合使用该方法,在某个时间段内始终在复用资源,不会频繁的创建线程、销毁线程
		//1.1、可使用最简单的方法
		ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

		//1.2、还可以指定生成的线程名称,自定义一个线程工厂即可
//		ExecutorService cachedThreadPoolCustomThreadName = Executors.newCachedThreadPool(
//				// 这里可以在实例化线程时传递自己的参数,例如给每个业务组的线程都增加一个前缀,在运行时很容易区分出来所属线程组
//				new BasicThreadFactory.Builder().namingPattern("sendMsg-%d").build());

		//2、使用线程池
		// 省略从队列中获取数据的步骤,这里应该触发很多次submit方法
		cachedThreadPool.submit(new Runnable() {
			// 使用匿名内部类来创建线程
			public void run() {
				// 这里直接从队列里面获取需要处理的数据,直接生成数据并持久化到数据库,逻辑直接结束
				// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
			}
		});
	}

}

核心参数解析:

ThreadFactory threadFactory(可选):线程池创建线程时的构建工厂,可以在线程池创建线程时改变线程实例的默认属性(可以看一下Thread的构造函数)。

其实该方法的内部是帮我们实例化了一个ThreadPoolExecutor,并保证核心线程数是0,最大线程数是Integer.MAX_VALUE,空闲存活时间为60秒,构建了一个阻塞队列

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

// ...
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue(),
                                      threadFactory);
    }

注意事项:若无法保证任务生产逻辑的任务量是可控的,请不要使用该方法创建线程池,否则可能会出现构建过多的线程实例,最大可达Integer.MAX_VALUE个线程实例,引发OOM或栈溢出

2.5、Executors.newScheduledThreadPool

其实还有个Executors.newSingleThreadScheduledExecutor()和Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory),单核心定时线程池,只是把核心线程数给了个默认值1而已

适用场景:延迟线程池,适合需要循环延迟执行的场景。

用法示例:

package com.thunisoft.test.ThreadPoolTest;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;

public class ThreadPoolTest {
	
	private static int N_NUM_10 = 10;
	
	private static int N_NUM_60 = 60;

	public static void main(String[] args) {
		// TODO Auto-generated method stub

	}

	public void testCachedThreadPool() {
		/**
		 * 业务场景假设:该应用为后台应用,程序启动后,每隔10秒校验一下指定应用端口的存活情况。
		 */
		//1、创建线程池--该方法可实现延迟执行和延迟执行+定时循环两种方式,具体选择哪一种请自行根据实际业务选择
		//1.1、可使用最简单的方法
		ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(N_NUM_10);

		//1.2、还可以指定生成的线程名称,自定义一个线程工厂即可
//		ScheduledExecutorService scheduledThreadPoolCustomThreadName = Executors.newScheduledThreadPool(N_NUM_10, 
//				// 这里可以在实例化线程时传递自己的参数,例如给每个业务组的线程都增加一个前缀,在运行时很容易区分出来所属线程组
//				new BasicThreadFactory.Builder().namingPattern("cs-%d").build());

		//2、使用线程池
		// 省略从队列中获取数据的步骤,这里应该触发很多次schedule方法
		// 特殊之处在于ScheduledExecutorService继承了ExecutorService,如果依然调用submit则失去了定时执行的特性
		// 这里直接从队列里面获取需要处理的机器,并进行存活情况校验及存活或不存活时的处理,逻辑直接结束
		// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
		
		scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
			// 使用匿名内部类来创建线程
			public void run() {
				// 这里直接从队列里面获取需要处理的数据,直接生成数据并持久化到数据库,逻辑直接结束
				// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
			}
			// 第一次任务延迟60秒(第一个参数),然后每隔10秒(第二个参数)执行一次
		}, N_NUM_60, N_NUM_10, TimeUnit.SECONDS);
	}

}

核心参数解析:

int nThreads:线程数大小,用于控制线程池的核心线程数及最大线程数。为啥就一个参数?对,没错,通过该方法创建的线程池核心线程数及最大线程数是一样的。

ThreadFactory threadFactory(可选):线程池创建线程时的构建工厂,可以在线程池创建线程时改变线程实例的默认属性(可以看一下Thread的构造函数)。

其实该方法的内部是帮我们实例化了一个ThreadPoolExecutor,并保证核心线程数为预设值,最大线程数为Integer.MAX_VALUE,空闲存活时间为0,构建了一个延迟队列。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

// ...
public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }



// ScheduledThreadPoolExecutor 类定义,继承了ThreadPoolExecutor,没啥本质区别

public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
// 省略部分代码...
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
}

注意事项:若无法保证任务生产逻辑的任务量是可控的,请不要使用该方法创建线程池,否则可能会出现构建过多的线程实例,最大可达Integer.MAX_VALUE个线程实例,引发OOM或栈溢出

2.6、new ThreadPoolExecutor()

适用场景:java自带线程池,适合你任何需要统一管理单个线程声明周期的场景,因为上面四种线程池的底层都是基于这个类的实例。

用法示例:网上一搜一大堆,或者等我下篇文章写出来后贴上来

注意事项:合理传参是比较重要的,否则会引发OOM栈溢出

三、结语

虽然Executors创建线程池很方便,但是还是推荐大家尽量少用。因为生产环境出现好几次因为直接使用Executors创建线程池且提交任务逻辑设计不合理,导致jvm崩溃的问题。

//规则描述:禁止直接使用Executors实例化线程池,推荐通过ThreadPoolExecutor实例化,并合理配置最大线程数(不易过大)及阻塞队列大小(必须指定队列大小,且不易过大)。
//规则说明:通过Executors创建的线程池封装过度,不合理的使用会导致队列超长或工作线程数过多从而导致OOM
//修改范围:查找源码中所有的Executors.new开头的代码,并以new ThreadPoolExecutor的方式重构
//修改示例:

        // 不推荐的写法
		// Executors帮我们默认构建了一个new LinkedBlockingQueue(),队列大小为Integer.MAX_VALUE
		// 存活任务对象的最大数量=最大线程数(这里给了4)+队列大小(默认是2的31次方-1就是2147483647)=2147483651个存活任务对象(不考虑运行时的垃圾回收)
		// 若任务生产速度很快,但任务消费很慢,就会导致队列数据暴涨,创建过多的任务对象且无法释放,最终导致OOM
		ExecutorService executorsNoncompliant = Executors.newFixedThreadPool(N_NUM_4, 
				new BasicThreadFactory.Builder().namingPattern("NoncompliantThreadPool-%d").build());
		// 推荐的写法(注意合理控制每个参数值)
		// 存活任务对象的最大数量=最大线程数(这里给了4)+队列大小(默认给了1000)=1004个存活任务对象(不考虑运行时的垃圾回收)
		// 若任务生产速度很快,但任务消费很慢,最多只能生产1004个对象便会触发线程池的拒绝策略(默认为抛出异常给调用者)
		// 注意考虑触发拒绝策略时的逻辑处理,是让主进程等待还是终止任务还是怎么办,千万不要不处理,否则也有可能把jvm搞挂
		ExecutorService executorsCompliant = new ThreadPoolExecutor(N_NUM_4, N_NUM_4, 0L,
				TimeUnit.MILLISECONDS, new LinkedBlockingQueue(N_NUM_1000), 
				new BasicThreadFactory.Builder().namingPattern("CompliantThreadPool-%d").build());
				

 

另外附上阿里编码规约中针对该方法的使用说明

https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E5%8D%8E%E5%B1%B1%E7%89%88%EF%BC%89.pdf

java 线程池 Executors 核心代码 原理详解 jdk 1.8_第1张图片

你可能感兴趣的:(经验分享,java)