前言

Java中的线程池是一个很重要的概念,它的应用场景十分广泛,可以被广泛的用于高并发的处理场景。J.U.C提供的线程池:ThreadPoolExecutor类,可以帮助我们管理线程并方便地并行执行任务。因此了解并合理使用线程池非常重要。

本文对线程池采用 3W 的策略结合源码进行思考逐层分析,即是什么为什么怎么做。

什么是线程池

线程池的本质是对任务和线程的管理,做到了将任务线程两者解耦。线程池对任务的管理可看作生产者消费者的关系,通过阻塞队列的存与取。阻塞队列缓存待执行的任务,工作线程从阻塞队列中获取任务。线程池对线程的管理,是结合线程池状态,已有线程的状态,核心线程数和最大线程数、阻塞队列状态做出增加、执行任务、回收、复用等操作,体现了享元模式和池化思想。

享元模式:

主要目的是实现对象的共享,运用共享技术有效地支持大量细粒度的对象,避免大量相类似的开销。当系统中对象多的时候可以减少内存的开销,通常与搭配工厂模式使用。

池化思想:

在多种使用对象的策略上,主张让使用的代价最小化。在重新创建对象的代价 远大于更换状态,复用对象的代价的前提下,将可以复用的对象放入池中待复用,以此降低使用的代价。

为什么要用线程池

线程池的优点,也是它为什么被流行使用的原因:

  • 重用线程池中的线程,避免因为线程的创建和销毁带来性能开销。
  • 能有效控制线程池的最大并发数,能提供定时执行以及定间隔循环执行等功能。
  • 线程池还提供了一种方法来约束和管理执行一组任务时消耗的资源(包括线程),避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
  • 可维护一些基本统计信息,比如已完成任务的数量。

主要的缺点:

  • 线程池的参数不存在完美的配置,高度依赖于开发者的经验,使用不当容易造成线上的危机
  • 线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,业界并没有一些成熟的经验策略帮助开发人员参考。

怎么用线程池

先了解线程池的相关重要概念:

Core and maximum pool sizes

核心线程数以及最大线程数,这是构造一个线程池所必需的参数。

不同的搭配会有不同效果的线程池,也是线程池判断在运行任务前是否需创建新线程的重要依据。

ThreadFactory

线程工厂,这是构造一个线程池的参数。

提供线程的创建,如果构建线程池不指定ThreadFactory,则使用默认线程工厂,创建的线程默认进入同一个ThreadGroup和默认线程优先级。

Keep-alive times

存活时间,这是构造一个线程池的参数。

如果线程池当前有超过corePoolSize大小的线程,如果非核心线程的空闲时间超过了keepAliveTime,则被视为可回收的多余线程,被终止

Queuing

任务/阻塞队列,这是构造一个线程池的参数。

不同类型的阻塞队列可以构造出适合不同场景的线程池。最常见的四种线程池就有着不同类型的阻塞队列。

作为任务的缓冲停留区,线程池管理线程的机制核心之一。

生产者消费者模式的体现,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

  • 在队列为空时,获取元素的线程会等待队列变为非空再尝试获取
  • 当队列满时,存储元素的线程会等待队列可用再尝试存储

Rejected tasks

拒绝任务后的策略,这是构造一个线程池的参数

加入任务时,根据线程池当前状态是否停止销毁、线程数是否以及饱和,判断是否拒绝本次任务的加入。若拒绝任务就会执行拒绝任务后的策略。默认的拒绝后的策略是抛出运行期异常RejectedExecutionException

On-demand construction

需求到达才创建,默认情况下,即使是核心线程最初也只有在新任务到达时才创建和启动,但是可以使用prestartCoreThread。如果使用非空队列构造池,可能需要预启动线程。

Hook methods

钩子方法

可重写的方法,beforeExecute(Runnable)afterExecute(Runnable,Throwable)terminated ,在执行每个任务之前和之后,线程池被完全终止后会被回调。可以用来执行特殊任务:重新初始化ThreadLocal变量、收集统计信息或添加日志条目。

最常见最常用的线程池

Executors类提供的也是最常见的线程池种类,配置,以及它们维护的阻塞队列类型,使用场景如下:

类型 核心线程数 最大线程数 阻塞队列 说明/使用场景
FixedThreadPool 构造时传入 与核心线程数相同 LinkedBlockingQueue 线程数量固定,只有核心线程并且不会被回收,没有超时机制
CachedThreadPool 0 Integer.MAX_VALUE SynchronousQueue 线程数量不固定的线程池,只有非核心的线程,当线程都处于活动状态时,直接创建新线程来处理新任务,否则就利用空闲的线程。处于空闲状态超过60s的线程被回收
ScheduledThreadPool 构造时传入 Integer.MAX_VALUE DelayedWorkQueue 非核心线程在闲置时立刻回收,主要用于执行定时任务和固定周期的重复任务
SingleThreadExecutor 1 1 LinkedBlockingQueue 只有一个核心线程,确保所有任务在同一线程中按顺序执行

分析创建这四个线程池的方法的源码,最后都来到了ThreadPoolExecutor类的ThreadPoolExecutor构造方法,由此可见ThreadPoolExecutor才是真正的线程池。Executors作为线程池工厂,提供的四种线程池是利用不同参数创建的适应不同使用场景的线程池。

//ThreadPoolExecutor.java

/**
* @param corePoolSize 核心线程数
* @param maximumPoolSize 最大线程数
* @param keepAliveTime 非核心线程闲置的超时时长
* @param unit 用于指定 keepAliveTime 参数的时间单位
* @param 任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中
* @param threadFactory 线程工厂,用于提供新线程
* @param handler 任务队列已满或者是无法成功执行任务时调用
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//···
}

线程池的简单使用

以手动创建一个核心数为5,最大线程数为7,空闲超时为20s,阻塞队列为数组实现的有界队列的ThreadPoolExecutor为例子:

        ExecutorService executor = new ThreadPoolExecutor(
5, 7, 20L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(8)
);
for(int i = 0; i < 9; i++){
final int index = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.valueOf(index)+ " " +Thread.currentThread().getName());
}
});
}

手动创建线程池的好处

阿里巴巴Java开发手册中使用强制标注:需通过手动创建 ThreadPoolExecutor 取代使用 Executors 提供的工厂方法。数据量并发量很大或难以把握时,应避免直接使用 Executors 提供的线程池,防止资源被耗尽

以CachedThreadPool为例子,CachedThreadPool将空闲线程销毁前的等待时间设置成了60s,同时阻塞队列类型是SynchronousQueue,不存储元素的队列。 CachedThreadPool 在一定程度上能够应对不断突增的并发任务,但是一旦任务量远远大于处理量,会造成线程数量的激增和资源的消耗,容易引发OOM。

手动创建线程池可以更好规范该线程池的职责,更好地管理这个线程池,让线程池在合适的场景下,可以用来处理适当的任务,而不是一颗随时会被引爆的炸弹。

总结

线程池,基于池化思想,体现了享元模式,可以用来管理线程并方便地并行执行任务的工具。本质上是对任务和线程解耦后进行管理,利用不同的构造参数可以构造出适合不同场景的线程池。优点是降低资源消耗提高响应速度提高线程的可管理性可拓展性良好。缺点是参数不易配置,出错后易造成OOM。

篇幅问题,对线程池的设计和管理机制的分析安排在下一篇文章~

参考资料

Java线程池的了解使用—筑基篇的更多相关文章

  1. Java线程池详解(二)

    一.前言 在总结了线程池的一些原理及实现细节之后,产出了一篇文章:Java线程池详解(一),后面的(一)是在本文出现之后加上的,而本文就成了(二).因为在写完第一篇关于java线程池的文章之后,越发觉 ...

  2. 深入浅出Java线程池:源码篇

    前言 在上一篇文章深入浅出Java线程池:理论篇中,已经介绍了什么是线程池以及基本的使用.(本来写作的思路是使用篇,但经网友建议后,感觉改为理论篇会更加合适).本文则深入线程池的源码,主要是介绍Thr ...

  3. Java线程池详解,看这篇就够了!

    构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?Runnable和Callable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段. 基础知识 Execu ...

  4. java 线程池第一篇 之 ThreadPoolExcutor

    一:什么是线程池? java 线程池是将大量的线程集中管理的类,包括对线程的创建,资源的管理,线程生命周期的管理.当系统中存在大量的异步任务的时候就考虑使用java线程池管理所有的线程.减少系统资源的 ...

  5. Java 线程池框架核心代码分析--转

    原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...

  6. Java线程池的那些事

    熟悉java多线程的朋友一定十分了解java的线程池,jdk中的核心实现类为java.util.concurrent.ThreadPoolExecutor.大家可能了解到它的原理,甚至看过它的源码:但 ...

  7. Java线程池的原理及几类线程池的介绍

    刚刚研究了一下线程池,如果有不足之处,请大家不吝赐教,大家共同学习.共同交流. 在什么情况下使用线程池? 单个任务处理的时间比较短 将需处理的任务的数量大 使用线程池的好处: 减少在创建和销毁线程上所 ...

  8. Java线程池与java.util.concurrent

    Java(Android)线程池 介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行 ...

  9. Java 线程池框架核心代码分析

    前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和资源消耗都是很高的.线程池应运而生,成为我们管理线程的利器.Java 通过Executor接口,提供了一种标准的方法将任务的提交过 ...

随机推荐

  1. 洛谷 P2607 [ZJOI2008]骑士 树形DP

    题目描述 Z国的骑士团是一个很有势力的组织,帮会中汇聚了来自各地的精英.他们劫富济贫,惩恶扬善,受到社会各 界的赞扬.最近发生了一件可怕的事情,邪恶的Y国发动了一场针对Z国的侵略战争.战火绵延五百里, ...

  2. Mysql如何取当日的数据

    下面的sql语句可以取出当日的数据 SELECT * FROM table WHERE 时间字段 BETWEEN DATE_FORMAT(NOW(),'%Y-%m-%d 00:00:00') AND ...

  3. 阐述Fetch.ai的能源市场优化

    原文链接:https://fetch.ai/explaining-fetch-ais-energy-market-optimization/ 阐述Fetch.ai的能源市场优化 2019年11月4日 ...

  4. 不会用Java Future,我怀疑你泡茶没我快, 又是超长图文!!

    你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough ...

  5. 绘图和可视化知识图谱-《利用Python进行数据分析》

    所有内容整理自<利用Python进行数据分析>,使用MindMaster Pro 7.3制作,emmx格式,源文件已经上传Github,需要的同学转左上角自行下载或者右击保存图片. 其他章 ...

  6. 不吹不擂,315 道 Python 面试题,欢迎挑战!

    各位大佬暂时先来315道题尝尝吧,后面有时间再继续补充. 有缘人如果看到这些题,不妨留言一下答案,来证明下你到底有多水,哈哈哈哈哈刀哈哈哈哈哈哈 第一部分 Python基础篇(80题) 1.为什么学习 ...

  7. GPO - Folder Mapping via GPO

    Create a Group Policy on AD DC Server. The GPO policy will come into effect on the next login, or us ...

  8. 带你上手阿里开源的 Java 诊断利器:Arthas

    本文适合有 Java 基础知识的人群. 本文作者:HelloGitHub-秦人 HelloGitHub 推出的<讲解开源项目>系列,今天给大家带来一款阿里开源的 Java 诊断利器 Art ...

  9. NIO入门之缓冲区Buffer

    缓存区 Buffer 是数据容器 ByteBuffer 可以存储除了 boolean 以外的其他 7 种Java基本数据类型,如 getInt.putInt Buffer 是抽象类,它有除了 Bool ...

  10. 常用CSS颜色表

    1.16进制的CSS颜色代码 > http://www.jsjtt.com/webkaifa/HTML/65.html