并发与多线程(四) --- 线程池

文章目录一、线程池的好处1.1 线程池的作用:1.2 线程是如何创建的二、使用步骤1.引入库2.读入数据总结一、线程池的好处线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源。线程的创建需要开辟虚拟机栈、本机方法栈、程序计数器等线程私有的内存空间。在线程销毁时需要回收这些资源。 频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程自身无法解决的。 所以需要通过线程池协调多个线程,并实现类似主次
摘要由CSDN通过智能技术生成


一、线程池的好处

线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源。线程的创建需要开辟虚拟机栈、本机方法栈、程序计数器等线程私有的内存空间。在线程销毁时需要回收这些资源。 频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程自身无法解决的。 所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。

1.1 线程池的作用:

  • 利用线程池管理并复用线程、控制最大并发数等。
  • 实现任务线程队列缓存策略和拒绝机制
  • 实现某些与时间相关的功能,如定时执行、周期执行等
  • 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易与搜索服务隔离开,避免各服务线程相互影响。

1.2 线程是如何创建的

首先从 ThreadPoolExecutor 构造方法分析,如何自定义ThreadFactory 和 RejectedExecutionHandler ,通过分析ThreadExecutor 的execute 和addWorker 两个核心方法,学习如何把任务线程加入到线程池中运行。
ThreadPoolExecutor 的构造方法如下:

public ThreadPoolExecutor(
	int corePoolSize,                            	// (第1个参数)
	int maximumPoolSize, 							// (第2个参数)
	long keepAliveTime, 							// (第3个参数)
	TimeUnit unit,									// (第4个参数)
	BlockingQueue<runnable> workQueue, 				// (第5个参数)
	ThreadFactory threadFactory, 					// (第6个参数)
	RejectedExecutionHandler handler) {
    			// (第7个参数)

	if(corePoolSize < 0 ||
	// maximumPoolSize 必须大于或等于1 也要大于或等于 corePoolSize   (第1处)
	maximumPoolSize <= 0 ||
	maximumPoolSize < corePoolSize ||
	keepAliveTime < 0)
		throw new IllegalArgumentException();
	(2)
	if (workQueue == null || ThreadFactory == null || handler == null)
		throw new NullPointerException();
	// 其他代码 ...
}

  • 第1个参数: corePoolSize
    表示常驻核心线程池数。如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于 0, 即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
  • 第2个参数:maximumPoolSize
    表示线程池能够容纳同时执行的最大线程数。从上方代码中第1处来看,必须大于或等于1。如果maximumPoolSize 与 corePoolSize 相等,即是固定大小线程池。
  • 第3个参数:keepAliveTime
    表示线程池中的线程空闲时间。当空闲时间达到keepAliveTime 值时,线程会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程数大于 corePoolSize 时,keepAliveTime 才会起作用。但是当ThreadPoolExecutor 的allowCoreThreadTimeOut 变量设置为true 时,核心线程超时后也被回收
  • 第4个参数:TimeUnit
    表示时间单位。keepAliveTime 的时间单位通常是TimeUnit.SECONDS 。
  • 第5个参数:workQueue
    表示缓存队列。当请求的线程数大于corePoolSize 时,线程进入BlockingQueue 阻塞队列,BlockingQueue 队列缓存达到上限后,如果还有新任务需要处理,那么线程池会创建新的线程,最大线程数为 maximumPoolSize 。例如一个生产消费模型队列,使用LinkedBlockingQueue 是单向链表,使用锁来控制入队和出队的原子性,两个锁分别控制元素的添加和获取。
  • 第6个参数: threadFactory
    表示线程工厂。它用来生成一组相同任务的线程。线程池的命令是通过给这个factory 增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。
  • 第7个参数: handler
    表示执行拒绝策略的对象。当第5个参数workQueue 的任务缓存区到达上限后,并且活动线程数大于maximumPoolSize 的时候,线程池通过该策略处理请求,这是一种简单的限流保护。像某年双十一没有处理好访问流量过载时的拒绝策略,导致内部测试页面被展示出来,使用户手足无措。友好的拒绝策略可以是如下三种
    (1)保存到数据库进行削峰填谷。在空闲时在提取出来执行。
    (2)转向某个提示页面。
    (3)打印日志。

从第2处来看,队列、线程工厂、拒绝处理服务都必须有实例对象,但在实际编程中,很少有程序员对这三者进行实例化,而通过Executor 这个线程池静态工厂提供默认实现,那么Executor 与 ThreadPoolExecutor 是什么关系呢?线程池相关类图如下图所示:

在这里插入图片描述

/**
* @param 线程任务
* @throws RejectedExecutionException 如果无法创建任何状态的线程任务
*/
void execute (Runnable command);

ExecutorService 接口继承了Executor 接口,定义了管理线程任务的方法。ExecutorService 的抽象类 AbstractExecutorService 提供了submit()、invokeAll() 等部分方法的实现,但是核心方法Executor.execute() 并没有在这里实现。因为所有的任务都在这个方法里执行,不同实现会带来不同的执行策略,这一点在后续的ThreadPoolExecutor 解析时,会进一步分析。通过Executors 的静态工厂方法可以创建三个线程池的包装对象:ForkJoinPool、ThreadPoolExecutor、ScheduledThreadPoolExecutor。

1.3 Executors 分析

Executors 核心的方法由五个

  • Executors.newWorkStealingPool:
    JDK8 引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU 数量设置为默认的并行度:
public statice ExecutorService newWorkStealingPool(){
   
	// 返回 ForkJoinPool (JDK7引入)对象,它也是AbstractExecutorService 的子类
	return new ForkJoinPool (Runtime.getRuntime().availableProcessors(),
	ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
}
  • Executors.newCachedThreadPool :
    maximumPoolSize 最大可以至Integer.MAX_VALUE , 是高度可伸缩的线程池,如果达到这个上限,相信没有服务器能够继续工作,肯定会抛出OOM 异常。keepAliveTime 默认为60 秒,工作线程处于空闲状态,则回收工作线程。如果认为数增加,再次创建出新线程处理任务。

  • Executors.newScheduledThreadPool:
    线程数最大至Integer.MAX_VALUE ,与上述相同,存在OOM 风险。它是ScheduledExecutorService 接口家族的实现类支持定时及周期性任务执行。相比Timer,ScheduledExecutorService 更安全,功能更强大,与newCachedThreadPool 的区别是不回收工作线程

  • Executors.newSingleThreadExecutor:
    创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行。

  • Executors.newFixedThreadPool :
    输入的参数即是固定线程数,既是核心线程数也是最大线程数,不存在空闲线程,所以keepAliveTime 等于 0 :

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

这里,输入的队列没有指明长度,下面为LinkedBlockingQueue 的构造方法:

public LinkedBlockingQueue () {
   
	this(Integer.MAX_VALUE);
}

使用这样的无界队列,如果瞬间请求非常大,会有OOM 的风险。除newWorkStealingPool 外,其他四个创建方式都存在资源耗尽的风险

Executors 中默认的线程工厂和拒绝策略过于简单,通常对用户不够友好。线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生成批号一样,为线程本身指定有意义的名称和相应的序列号。拒绝策略应该考虑到业务场景,返回相应的提示或友好地跳转

1.3.1 ThreadFactory 示例


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值