一、前言

  随着业务的发展,单线程已经远远不能满足,随即就有多线程的出现。多线程虽然能解决单线程解决不了的事情,但是它也会给你带来额外的问题。比如成千上万甚至上百万的线程时候,你系统就会出现响应延迟、卡机、甚至直接卡死的情况。为什么会出现这样的原因呢?因为为每个请求创建一个新线程的开销很大:在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多

  除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。在一个 JVM里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。所以为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目。而线程池为线程生命周期开销问题和资源不足问题提供了解决方案。

二、那么线程池有哪些作用呢?

  1、降低资源消耗,防止资源不足。合理配置线程池中的线程大小,防止请求线程猛增;另外通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2、提高响应速度。线程池可以通过对多个任务重用线程,在请求到达时线程已经存在(如果有空闲线程时),所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。
  3、提高线程的可管理性。使用线程池可以统一分配、调优和监控线程。

  上面知道了线程池的作用,那么线程池它是如何工作的呢?其使用核心类是哪一个呢?所以要做到合理利用线程池,必须对其实现原理了如指掌。

三、线程池中核心类:ThreadPoolExecutor

  java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,所以必须了解这个类的用法及其内部原理,下面我们来看下ThreadPoolExecutor类的具体源码解析。

3.1  继承关系

  通过类的继承关系可以得知哪些方法源于哪里(具体请看代码),下面直接给出类的继承结构的图:


3.2 构造方法   

  在ThreadPoolExecutor类中提供了四个构造方法:

         // 五个参数的构造函数
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
// 六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
} //六个参数的构造函数 -2
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
// 七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

  从源代码中发现前面三个构造器都是调用的第四个构造器进行的初始化工作,那就以第四个构造函数为例,解释下其中各个参数的含义(留意源码中每个字段上的注释):

  1. int corePoolSize核心线程数
    • 在创建了线程池后,默认情况下线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务;
    • 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使有其他空闲的基本线程能够执行新任务也会创建线程(比方说:coreSize=5时,一开始只有一个任务会创建一个线程,等执行完后又来了一个任务时,依然会创建一个线程不会使用第一个线程)
    • 当线程池中的线程数目达到corePoolSize后就不再创建线程,会把到达的任务放到缓存队列当中等待执行。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程,另外prestartCoreThread方法也会启动核心线程,不过每次只能启动一个。

  2. int maximumPoolSize线程池允许创建的最大线程数。

    • 如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果

  3. long keepAliveTime空闲线程等待超时的时间

    • 默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
    • 但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的空闲线程数为0;
    • 如果任务很多且每个任务执行的时间比较短,则可以调大时间,提高线程利用率。

  4. TimeUnit unit参数keepAliveTime的时间单位。共有七种单位,如下:

public enum TimeUnit {
/**
* 纳秒=千分之一微妙
*/
NANOSECONDS {...}, /**
* 微妙=千分之一毫秒
*/
MICROSECONDS {...}, /**
* 毫秒
*/
MILLISECONDS {...}, /**
* 秒
*/
SECONDS {...}, /**
* 分钟
*/
MINUTES {...}, /**
* 小时
*/
HOURS {...}, /**
* 天
*/
DAYS {...};
}

  5. BlockingQueue<Runnable> workQueue: 任务队列,用于保存等待执行任务的阻塞队列。队列也有好几种详细请看这里,这里就不做解释了。

  6. ThreadFactory threadFactory:线程工厂,主要用于创建线程。其中可以指定线程名字(千万别忽略这件小事,有意义的名字能让你快速定位到源码中的线程类)

  7. RejectedExecutionHandler handler:饱和策略,当队列和线程池都满了,说明线程处于饱和状态,那么后续进来的任务需要一种策略处理。默认情况下是AbortPolicy:表示无法处理新任务时抛出异常。线程池框架提供了以下4中策略(当然也可以自己自定义策略:通过实现RejectedExecutionHandler接口自定义策略):

    • AbortPolicy:不处理新任务,抛出异常
    • CallerRunsPolicy:只用调用者所在的线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务, 并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。

 3.3 重要参数方法和方法解读

  1. 线程池状态

     // 初始值 -536870912
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 初始值 29
private static final int COUNT_BITS = Integer.SIZE - 3;
// 初始值 536870911
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// RUNNING状态:接受新任务并处理排队任务
private static final int RUNNING = -1 << COUNT_BITS;
// SHUTDOWN状态:不接受新任务,但处理排队任务
private static final int SHUTDOWN = 0 << COUNT_BITS;
// STOP状态:不接受新任务,不处理排队任务,并中断正在进行的任务
private static final int STOP = 1 << COUNT_BITS;
// All tasks have terminated, workerCount is zero, the thread transitioning to state TIDYING will run the terminated() hook method
private static final int TIDYING = 2 << COUNT_BITS;
// TERMINATED: terminated() has completed
private static final int TERMINATED = 3 << COUNT_BITS;
// 获取线程池状态,取前三位
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 获取当前正在工作的worker,主要是取后面29位
private static int workerCountOf(int c) { return c & CAPACITY; }
// 生成ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }

  当创建线程池后,初始时,线程池处于RUNNING状态;

  RUNNING -> SHUTDOWNN:如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

  (RUNNING or SHUTDOWN) -> STOP:如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

  SHUTDOWN -> TIDYING or STOP -> TIDYING :当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

  2. 线程池中的线程初始化

  在说corePoolSize参数时有说到初始化线程池的两个方法,其实在默认情况下,创建线程池之后线程池中是没有线程的,需要提交任务之后才会创建线程。所以如果想在创建线程池之后就创建线程的话,可以通过下面两个方法创建:

/**
* 单个创建核心线程
*/
public boolean prestartCoreThread() {
return workerCountOf(ctl.get()) < corePoolSize &&
addWorker(null, true);
}
/**
* 启动所有核心线程
*/
public int prestartAllCoreThreads() {
int n = 0;
// 添加工作线程
while (addWorker(null, true))
++n;
return n;
}

  3. 创建线程:addWorker()

private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
// 获取运行状态
int rs = runStateOf(c);
/**
* 如果当前的线程池的状态>SHUTDOWN 那么拒绝Worker的add 如果=SHUTDOWN
* 那么此时不能新加入不为null的Task,如果在WorkCount为empty的时候不能加入任何类型的Worker,
* 如果不为empty可以加入task为null的Worker,增加消费的Worker
*/
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false; for (;;) {
// 获取有效线程数,并判断//如果当前的数量超过了CAPACITY,或者超过了corePoolSize和maximumPoolSize(试core而定),则直接返回
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// //CAS尝试增加线程数,如果失败,证明有竞争,那么重新到retry。
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// 继续判断当前线程池的运行状态
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
} /**
* 新建任务
*/
Worker w = new Worker(firstTask);
Thread t = w.thread; final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int c = ctl.get();
int rs = runStateOf(c);
/**
* rs!=SHUTDOWN ||firstTask!=null
*
* 同样检测当rs>SHUTDOWN时直接拒绝减小Wc,同时Terminate,如果为SHUTDOWN同时firstTask不为null的时候也要Terminate
*/
if (t == null ||
(rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null))) {
decrementWorkerCount();
tryTerminate();
return false;
} workers.add(w); int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
} finally {
mainLock.unlock();
} t.start();
//Stop或线程Interrupt的时候要中止所有的运行的Worker
if (runStateOf(ctl.get()) == STOP && ! t.isInterrupted())
t.interrupt();
return true;
}

  从上面可以看出:

    在rs>SHUTDOWN时,拒绝一切线程的增加,因为STOP是会终止所有的线程,同时移除Queue中所有的待执行的线程的,所以也不需要增加first=null的Worker了。

    其次,在SHUTDOWN状态时,是不能增加first!=null的Worker的,同时即使first=null,但是此时Queue为Empty也是不允许增加Worker的,SHUTDOWN下增加的Worker主要用于消耗Queue中的任务。

    SHUTDOWN状态时,是不允许向workQueue中增加线程的,isRunning(c) && workQueue.offer(command) 每次在offer之前都要做状态检测,也就是线程池状态变为>=SHUTDOWN时不允许新线程进入线程池了。

  
  4、执行任务:execute()
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
* 原注释已经讲的很清楚了,主要分三步进行:
*/
int c = ctl.get();
// 1、如果线程数小于基本线程数,则创建线程并执行当前任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2、如果任务可以排队,则会重新检查看是否可以启动新的任务还是拒绝任务
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3、如果我们无法排队任务,那么我们尝试添加一个新线程。 如果失败,我们知道我们已关闭或饱和,因此拒绝该任务。
else if (!addWorker(command, false))
reject(command);
}

  注意:该方法是没有返回值的,如果想获取线程执行后的结果可以调用submit方法(当然它底层也是调用execute()方法)

  5、线程池关闭:

  ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

四、线程池工作流程图

  从上线的源码分析后,应该知道线程池处理任务的大概流程了,下面统一梳理下当线程池接到任务时的处理流程:

  1、线程池首先会判断核心线程池是否已满(核心线程数是否超过corePoolSize),若没有则创建新的核心线程来处理任务,否则进行第二步;

  2、接着会判断阻塞队列是否已满(所以推荐使用有界队列),如果没有满则进入阻塞队列等待执行,否则进行第三步;

  3、然后线程池会判断整个线程池是否已满(整个线程数是否超过maximunPoolSize),若没有则创建新线程处理任务,否则交个饱和策略处理新的任务。

五、关于线程池使用的注意事项

  1、创建线程或线程池时请指定有意义的线程名称,方便回溯。来源《阿里巴巴 Java开发手册》

  2、线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。来源《阿里巴巴 Java开发手册》

    说明:Executors 返回的线程池对象的弊端如下:

    1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

     2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

  3、合理配置线程池大小,可以从以下几个角度来进行分析:

    • 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
    • 任务的优先级:高,中和低。
    • 任务的执行时间:长,中和短。
    • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

    比方说:对于 CPU 密集型的计算场景,理论上“线程的数量 = CPU核数”是最合适的。

    如果是IO密集型任务,参考值可以设置为CPU 核数 * [ 1 +(I/O 耗时 / CPU耗时)]

    注意:以上值仅供参考,需要根据具体实际情况(压测)而定。

  4、建议使用有界队列

    • 有界队列起码会是maximumPoolSize参数生效。
    • 有界队列能增加系统的稳定性和预警能力,根据需要设置队列长度(队列长度最好也不要太大,太大可能会堆积大量的请求,从而导致OOM)

  5、合理设置空闲线程等待时间。

    如果任务很多且每个任务执行的时间比较短,则可以调大时间,提高线程利用率。

六、参考资料

https://www.cnblogs.com/dolphin0520/p/3932921.html

http://ifeve.com/java-threadpool/

《Java并发编程的艺术》

《阿里巴巴Java开发手册》

Java线程池,你了解多少?的更多相关文章

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

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

  2. Java线程池使用说明

    Java线程池使用说明 转自:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极 ...

  3. (转载)JAVA线程池管理

    平时的开发中线程是个少不了的东西,比如tomcat里的servlet就是线程,没有线程我们如何提供多用户访问呢?不过很多刚开始接触线程的开发攻城师却在这个上面吃了不少苦头.怎么做一套简便的线程开发模式 ...

  4. Java线程池的那些事

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

  5. 四种Java线程池用法解析

    本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 http://www.jb51.net/article/81843.htm 1.new Thread的弊端 执行一个异步任务你还只是如下 ...

  6. Java线程池的几种实现 及 常见问题讲解

    工作中,经常会涉及到线程.比如有些任务,经常会交与线程去异步执行.抑或服务端程序为每个请求单独建立一个线程处理任务.线程之外的,比如我们用的数据库连接.这些创建销毁或者打开关闭的操作,非常影响系统性能 ...

  7. Java线程池应用

    Executors工具类用于创建Java线程池和定时器. newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程.在任意点,在大多数 nThread ...

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

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

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

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

  10. [转 ]-- Java线程池使用说明

    Java线程池使用说明 原文地址:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1. ...

随机推荐

  1. ASP.Net Core 2.2 MVC入门到基本使用系列 (一)

    本教程会对基本的.Net Core 进行一个大概的且不会太深入的讲解, 在您看完本系列之后, 能基本甚至熟练的使用.Net Core进行Web开发, 感受到.Net Core的魅力. 本教程知识点大体 ...

  2. NetCore入门篇:(九)Net Core项目使用Session及用Redis做分布式

    一.简介 1.因为Net Core默认是没有启动Session功能的,如果需要使用,需要通过代码开启. 2.本篇说明如果启用默认Session实现,即Session存到内存中. 3.本篇扩展说明如何用 ...

  3. keil小技能随用随定义

    大家都知道在C语言编程时一般都是先定义再使用这个变量的,不允许在语句的后面再定义,但是有时候我们会在KEIL中发现有些人使用变量就在语句后定义,这时我们自己去尝试却发现总是失败,这是为何呢? 原来是我 ...

  4. 我的第一个网络爬虫 C#版 福利 程序员专车

    最近在自觉python,看到了知乎上一篇文章(https://www.zhihu.com/question/20799742),在福利网上爬视频... 由是我就开始跟着做了,但答主给的例子是基于pyt ...

  5. c# 合并重叠时间段的算法

    c# 合并重叠时间段的算法 一.采用非排序: 方案一: 使用递归算法,如不喜欢递归的伙伴们,可以使用whie代替. 1.文件:Extract_Chao.cs(核心) using System; usi ...

  6. oracle 游标简单案例

    oracle  游标简单案例 一.案例: DECLARE IDO NUMBER; DABH CHAR); t_count ); CURSOR TJ_CURSOR IS SELECT IDO,DABH ...

  7. AtomicBoolean

    它的两种用法: 1.保证某段语句只执行一次. 首先我们要知道compareAndSet的作用,判断对象当时内部值是否为第一个参数,如果是则更新为第二个参数,且返回ture,否则返回false.那么默认 ...

  8. day74天中间件介绍

    一. importlib settings 执行结果: 两个process_request  process_response按照注册顺序的倒叙进行执行 PROCESS_VIEW  Process_v ...

  9. 关于popup

    p1.html:点击添加按钮,开启窗口,打开p2.html,填写数据后返回p3.html,p3.html将数据回传到p1.html,且关闭自己   p1.html: <!DOCTYPE html ...

  10. CSS3的三大特性

    在学习CSS 的时候,我们必须要熟练和理解CSS 的三大特性,那么CSS 的三大特性又是什么呢? CSS 的三大特性:层叠 继承 优先级  ,CSS 三大特性是我们学习CSS 必须掌握的三个特性. 首 ...