前言

Java中的线程池用过吧?来说说你是怎么使用线程池的?这句话在面试过程中遇到过好几次了。我甚至这次标题都想写成【Java八股文之线程池】,但是有点太俗套了。虽然,线程池是一个已经被说烂的知识点了,但是还是要写这一篇用来加深自己的印象,但是想使用一个特殊的方式写出来。

线程池

使用线程池的目的

先说一下我们为什么要使用线程池?

  • 线程是稀缺资源,不能频繁的创建。而且创建和销毁线程也是比较占用系统开销的。
  • 为了做到解耦,线程的创建与执行任务分开,方便对线程进行维护。
  • 为了复用,前面也说了创建和销毁线程比较耗系统开销,那么创建出来线程放到一个池子里,可以给其他任务进行复用。
线程池是如何一步一步创建的

第一版

正常的我们在创建一个线程去执行任务的时候是这样的:

new Thread(r).start();

但是这是最基本的方式,我们的项目中有可能很多地方都需要创建一个新的线程。这个使用为了减少重复代码,我们会把这段创建线程的代码放的一个工具类里面,然后对外提供工具方法,使用的时候直接调用此方法即可。

第二版

/**
* 先定义接口(任务执行器)
*/
public interface Executor {
/**
* 执行任务
* @param runnable 线程任务
*/
void execute(Runnable runnable);
}
/**
* 实现:直接创建线程。
*/
class ExecutorImpl implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}

这种方式实现了创建线程的代码的复用,但是并没有实现线程资源的复用,当有1000个地方需要线程的时候,会创建1000个线程。

第三版

为了实现资源也复用,增加一个阻塞队列,当来了创建线程的任务的时候,先放到队列里,然后再用一个线程(Worker),来处理任务。这样就完成了线程资源的复用了,全程只有一个线程在来回的复用,一直在处理队列中的任务。

通过上面的方式,实现了线程资源的复用,并且也起到提交任务和处理任务之间的解耦。但是只有一个线程处理任务,会有瓶颈的,所以具体需要多少线程来处理任务最好是根据具体的业务场景来确定,这样我们把这个值,设置成一个参数,当创建线程池的时候传入,就叫corePoolSize吧。

而且任务队列最好也要有容量,但也应该是根据业务场景来配置容量,而且任务队列还可以定制一些规则,例如:按照一定的规则出队。所以我们把任务队列也配置成参数,在创建线程池的时候传入。参数名称就叫:workQueue吧。

当队列中任务满了之后,任务就会被抛弃,但是如果是重要业务任务,还不能抛弃,所以,当队列中任务满了之后,在线程池没有资源处理任务的时候,拒绝策略,我们也根据业务场景来确定,这样也在创建的时候传入一种拒绝策略,参数名称就叫:rejectedExecutionHandler

继续优化

虽然多了上面的三个参数后效果优化了不少,但是还可以继续优化:

  • 并不用上来就创建corePoolSize数量的线程,我们可以增加了一个变量workCount,来记录已经创建出来了工作线程,这样在初始化的时候只有workCount<corePoolSize的时候,我们才创建线程来执行任务,当workCount>CorePoolSize的时候,再来了任务,就去进队列。
  • 在增加拒绝策略的时候,我定义一个接口:RejectedExecutionHandler,然后使用者可以自己去实现这个接口,来完成自己的拒绝策略。
  • 增加一个线程工厂的入参:ThreadFactory,这样保证每次创建线程的时候不用手动去创建线程了,而是通过ThreadFactory来获取线程,并且也可以增加一些线程的标识。

第四版

虽然说第三版的线程池已经可以应对日常工作中的情况了,但是还是不够有弹性,所谓的弹性就是指,在任务提交频繁时应该处理能力提高,任务提交不频繁时处理能力应该降低

上面这版线程池就不够弹性。

如果某个时间段,任务提交量剧增,这个时候,corePoolSize和队列都满了,再来提交任务就只能走拒绝策略了。

你或许会想到,那我可以增大corePoolSize的值,这样就会创建出来更多的线程来处理任务,但是这个任务提交量剧增,只是某个时间段,过了这个时间段之后,创建出来这么多的线程,可以大部分都会是空闲的状态。这样也是浪费资源了。

这样就导致了一个两难的情况,corePoolSize的值设置太大了也不好,设置太小了也不好。

这个时候,为让线程池做到弹性伸缩,我们可以为他再添加一个参数:maximumPoolSize,这个参数代表的意思是最大线程数。

corePoolSizeworkQueue都满了的时候,新提交的任务仍然可以创建新线程来进行处理,这些超过corePoolSize创建出来的线程,被称为非核心线程。当corePoolSize与非核心线程数量的和等于maximumPoolSize再执行拒绝策略。



通过这样的方式,corePoolSize,负责平时情况的线程使用量,maximumPoolSize负责提交任务高峰时的,临时扩充容量。

但是目前这样的方式只是考虑到了提交任务量高峰时期的扩充,但这个高峰期只是暂时的,过了这个高峰期,非核心线程一直放着也是浪费资源,所以我们再设定一个非核心线程的空闲活跃时间的参数:keepAliveTime,这样当非核心线程数,空闲时间超过这个值就销毁线程,释放资源。



这一版的线程池,做到了在提交任务高峰时可临时扩容,低谷时又可及时回收非核心线程,从而节省资源。真正的做到了收放自如

通过上面几版线程池的改进,最终改进成了和Java中的线程池原理基本相似了。这样也能更透彻的理解创建线程池时要传入的这几个关键参数的意义了。

下面说几个线程池常见的考察点

Java中的线程池的阻塞队列都有哪几种

  • ArrayBlockingQueue : 有界队列,按照阻塞的先后顺序访问队列,默认情况下不保证线程公平的访问队列~如果要保证公平性,会降低一定的吞吐量。底层是靠ReentrantLock来实现的,每一个方法中,都是靠ReentrantLock加锁来完成阻塞。
 public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
  • LinkedBlockingQueue:基于链表的阻塞队列,按照先进先出的顺序排列,在不设置队列长度的时候默认Integer.MAX_VALUE。所以认为当不设置队列长度时,LinkedBlockingQueue为无解队列。当指定了队列长度后变为有界队列,通常LinkedBlockingQueue的吞吐量要高于ArrayBlockingQueue;
  • SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。在不允许任务在队列中等待的时候可以使用此队列。
  • DelayQueue:延迟获取元素队列,按照指定时间后获取,为无界阻塞队列。
  • PriorityBlockingQueue:优先级排序队列,按照一定的优先级对任务进行排序,默认是小顶堆。
  • LinkedBlockingDeque:基于链表的双端阻塞队列。

Java提供了哪几个默认的线程池,为什么实际开发中不建议直接使用?

  • Executors.newCachedThreadPool();:阻塞队列采用的SynchronousQueue,所以是不存储等待任务的,并且最大线程数的值是Integer.MAX_VALUE。所以当任务提交量高峰时,相当于无限制的创建线程。并且空闲时间是60秒,QPS高峰期最终会将服务器资源耗尽,所以真正实际应用中不建议使用。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
  • Executors.newFixedThreadPool(int nThreads);:可重用固定线程数的线程池,源码如下:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
} public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

核心线程数和最大线程数相等的线程池,并且阻塞任务队列还是一个无解队列,这样,当处理任务的线程数量达到核心线程数时,再提交的任务都会进行到阻塞队列里,但是阻塞队列是无界的,这样就提交任务高峰期有可能会造成任务一直堆积在队列里,超出内存容量最终导致内存溢出。

  • Executors.newScheduledThreadPool(int corePoolSize);:一个定长线程池,支持定时及周期性任务执行,这个线程池的最大线程数也是Integer.MAX_VALUE,可以理解为会无限创建线程。存在将资源耗尽的风险,所以一般场景下不建议使用。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
  • Executors.newSingleThreadExecutor();

    这种线程池,会创建一个线程数固定是1的线程池,并且任务队列是无解的LinkedBlockingQueue,存在任务队列无限添加造成OOM的风险。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
  • Executors.newWorkStealingPool();:一个具有抢占式操作的线程池。

    参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别,前面4种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作,哪个线程抢到任务就执行。
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}

Java中的线程池提供了哪几种拒绝策略

  • AbortPolicy:该策略默认是饱和策略。当不能在处理提交的任务时,直接抛出RejectedExecutionException,使用者可以自行捕获此异常。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
  • CallerRunsPolicy:该策略是在线程池处理不了任务时,交给提交任务的主线程去处理任务,主线程在处理任务的时候,不能在提交任务了,这样线程池就可以有时间去处理堆积的任务了。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
  • DiscardOldestPolicy:该策略是,抛弃最老的任务,然后再尝试提交任务,若阻塞队列使用PriorityBlockingQueue优先级队列,将会导致优先级最高的任务被抛弃,所以在阻塞队列为PriorityBlockingQueue时,不建议使用此策略。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
  • DiscardPolicy:这是一个比较任性的策略,当线程池处理不了任务时,直接抛弃,再来了新任务也直接抛弃。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
  • RejectHandler

    直接抛拒绝异常。
public void rejectedExecution(Runnable r, java.util.concurrent.ThreadPoolExecutor executor) {
throw new RejectedExecutionException();
}

Java中线程池核心线程数与最大线程数该如何配置

可以根据提交的任务不同,将线程池分开。

  • 处理CPU密集型任务,线程数量应该较少,可为N(CPU核数)+1N(CPU核数) * 2,因为此时线程一定调度到某个CPU执行,若任务本身是CPU绑定的任务,那么过多的线程只会增加线程切换的开销,而不能提升吞吐量,但可能需要较长队列做缓冲。
  • I/O密集型任务,执行较慢、数量不大的IO任务,要考虑更多线程数,而无需太大队列。相比计算型任务,需多一些线程,要结合具体的 I/O 阻塞时长考虑。

但是实际情况下,有些任务是既耗CPU资源,又占用I/O资源的。所以这个时候可以采用类似美团技术提出方案,实时的监控线程池状态信息,然后对线程池的数据进行调整。

在监控线程池的时候可以使用如下几个线程池属性:

  • getTaskCount():线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
  • largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过,如该数值等于线程池的最大线程数量,则表示线程池曾经满过。
  • getPoolSize():线程池的线程数量,如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
  • getActiveCount():获取活动的线程数。

参考:

你管这破玩意叫线程池?

[Java并发编程艺术]

Java中的线程池用过吧?来说说你是怎么理解线程池吧?的更多相关文章

  1. Eclipse中的快捷键快速生成常用代码(例如无参、带参构造,set、get方法),以及Java中重要的内存分析(栈、堆、方法区、常量池)

    (一)Eclipse中的快捷键:  ctrl+shift+f自动整理选择的java代码 alt+/ 生成无参构造器或者提升信息 alt+shift+s+o 生成带参构造 ctrl+shift+o快速导 ...

  2. Java中Class和单例类的作用与类成员的理解

    Java中Class类的作用与深入理解 在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识.这个信息跟踪着每个对象所属的类.JVM利用运行时信息选择相应的方法执行.而保存 ...

  3. Java中线程池,你真的会用吗?

    在<深入源码分析Java线程池的实现原理>这篇文章中,我们介绍过了Java中线程池的常见用法以及基本原理. 在文中有这样一段描述: 可以通过Executors静态工厂构建线程池,但一般不建 ...

  4. Java中线程池,你真的会用吗?ExecutorService ThreadPoolExcutor

    原文:https://www.hollischuang.com/archives/2888 在<深入源码分析Java线程池的实现原理>这篇文章中,我们介绍过了Java中线程池的常见用法以及 ...

  5. java中的线程问题(一)什么是线程。

    线程--什么是进程 进程--概念 要解释线程,就必须明白什么是进程. 什么是进程呢? 进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的IE浏览器,就启动了一个进 ...

  6. JAVA中怎么处理高并发的情况

    一.背景综述 并发就是可以使用多个线程或进程,同时处理(就是并发)不同的操作. 高并发的时候就是有很多用户在访问,导致系统数据不正确.糗事数据的现象.对于一些大型网站,比如门户网站,在面对大量用户访问 ...

  7. Java NIO之Java中的IO分类

    前言 前面两篇文章(Java NIO之理解I/O模型(一).Java NIO之理解I/O模型(二))介绍了,IO的机制,以及几种IO模型的内容,还有涉及到的设计模式.这次要写一些更贴近实际一些的内容了 ...

  8. JAVA中LOCK

    原文链接:http://www.cnblogs.com/dolphin0520/p/3923167.html 一.synchronized的缺陷 我们知道如果一个代码块被synchronized修饰了 ...

  9. Java并发编程(十一)-- Java中的锁详解

    上一章我们已经简要的介绍了Java中的一些锁,本章我们就详细的来说说这些锁. synchronized锁 synchronized锁是什么? synchronized是Java的一个关键字,它能够将代 ...

  10. Java中的锁(转)

    Java中的锁 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂.因为锁(以及其它更高级的线程同步机制)是由synchronized同步 ...

随机推荐

  1. 第50天学习打卡(CSS 圆角边框 盒子阴影 定位)

    4.4圆角边框 圆角边框:  <!DOCTYPE html> <html lang="en"> <head>     <meta char ...

  2. GDB调试:从入门到入土

    GDB是类Unix操作糸统下使用命令行调试的调试软件,全名GNU Debugger,在NOI系列竞赛使用的NOI Linux系统中起很大作用(如果不想用毒瘤Guide或直接输出)(XXX为文件名) 1 ...

  3. java list集合遍历时删除元素

    转: java list集合遍历时删除元素 大家可能都遇到过,在vector或arraylist的迭代遍历过程中同时进行修改,会抛出异常java.util.ConcurrentModification ...

  4. 2020年12月-第02阶段-前端基础-CSS Day03

    CSS Day03 盒子模型(CSS重点) css学习三大重点: css 盒子模型 . 浮动 . 定位 主题思路: 理解: 1.能说出盒子模型有那四部分组成 2.能说出内边距的作用以及对盒子的影响 3 ...

  5. Linux下制作Windows启动U盘的工具

    Linux下制作Windows启动U盘的工具 很多人说Linux下制作Windwos启动盘要用GRUB4DOS建立引导,其实不用,有专门的工具的,就像Windows下有Rufus制作Linux启动U盘 ...

  6. Java 并发编程小册整理好了

    Java 有并发,并发知识之大,一口吃不下 这曾是我不愿意触碰的知识角 多次一头扎进并发,无功而返 为应对面试,临时苦苦记忆,不成体系 这一次我决定从基础开始,攻克它 12,0000 字 68Mb 高 ...

  7. 如何在Bash脚本中引入alias

    更多精彩内容,请关注微信公众号:后端技术小屋 alias的使用 在日常开发中,为了提高运维效率,我们会用alias(命令别名)来定义命令的简称.比如在~/.bash_profile中添加: alias ...

  8. 性能追击:万字长文30+图揭秘8大主流服务器程序线程模型 | Node.js,Apache,Nginx,Netty,Redis,Tomcat,MySQL,Zuul

    本文为<高性能网络编程游记>的第六篇"性能追击:万字长文30+图揭秘8大主流服务器程序线程模型". 最近拍的照片比较少,不知道配什么图好,于是自己画了一个,凑合着用,让 ...

  9. FreeBSD ibus输入法框架配置

    FreeBSD ibus输入法框架配置 ibus输入法框架配置.xinitrc中增加XIM=ibus; export XIMGTK_IM_MODULE=ibus; export GTK_IM_MODU ...

  10. VirtualBOX 虚拟机 FreeBSD配置

    pkg装virtualbox-ose-additions, 再将 Section "Device"Identifier "Card0"Driver " ...