前言

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线程池详解,看这篇就够了!

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

  3. java 线程池第一篇 之 ThreadPoolExcutor

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

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

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

  5. Java线程池的那些事

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

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

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

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

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

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

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

  9. java线程池的使用与详解

    java线程池的使用与详解 [转载]本文转载自两篇博文:  1.Java并发编程:线程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html   ...

  10. JAVA线程池应用的DEMO

    在做很多高并发应用的时候,单线程的瓶颈已经满足不了我们的需求,此时使用多线程来提高处理速度已经是比较常规的方案了.在使用多线程的时候,我们可以使用线程池来管理我们的线程,至于使用线程池的优点就不多说了 ...

随机推荐

  1. 常见的js 里对数字进行处理的函数方法集合

    常见的对小数值舍入为整数的几个方法:Math.ceil().Math.floor()和Math.round(). 这三个方法分别遵循下列舍入规则: Math.ceil()执行向上舍入,即它总是将数值向 ...

  2. HTML5常用的方法

    1.html禁止手机页面放大缩小 在页面head中加入<meta name="viewport" content="width=device-width, init ...

  3. Java中的继承与静态static等的执行先后顺序

    package extend; public class X { Y y=new Y(); static{  System.out.println("tttt"); } X(){  ...

  4. 【Python】Python与文本处理langid工具包的文本语言检测和歧视

    1.问题的叙述性说明 使用Python文本处理.文字有时被包括中国.英语.在日本和其他语言文字,进行处理.这个时候就须要判别当前文本是属于哪个语系的. Python中有个langid工具包提供了此功能 ...

  5. Struts2基础学习(一)&mdash;初识Struts2

      目录 一.什么是Struts2 二.搭建Struts2的开发环境 三.Struts2的配置文件 四.MVC模式 一.什么是Struts2      Struts2是一个非常优秀的MVC框架,由传统 ...

  6. 安装puppet

    安装puppet服务 先安装ruby语言包.ruby标准库.ruby shadow库 yum install -y ruby ruby-libs ruby-shadow 2.需要添加EPRL库,来支持 ...

  7. Python——day11 函数(对象、名称空间、作用域、嵌套、闭包)

    一.函数对象  函数名就是存放了函数的内存地址,存放了内存地址的变量都是对象,即 函数名 就是 函数对象  函数对象的应用 1. 可以直接被引用  fn = cp_fn 2 .可以当作函数参数传递 c ...

  8. charCodeAt与fromCharCode

    charCodeAt() 方法可返回指定位置的字符的 Unicode 编码 这个返回值是 0 - 65535 之间的整数. stringObject.charCodeAt(index) /* a-z  ...

  9. 转载一篇关于toString和valueOf

    可以这样说,所有JS数据类型都拥有valueOf和toString这两个方法,null除外.它们俩解决javascript值运算与显示的问题.在程序应用非常广泛.下面我们逐一来给大家介绍下. Java ...

  10. 如何在Ubuntu 16.04上安装配置Redis

    如何在Ubuntu 16.04上安装配置Redis Redis是一个内存中的键值存储,以其灵活性,性能和广泛的语言支持而闻名.在本指南中,我们将演示如何在Ubuntu 16.04服务器上安装和配置Re ...