Java线程池原理(一)


Java线程池原理(一)

众所周知Executor框架中的ExecutorService接口的实现就是ThreadPoolExecutor线程池,本文《Java线程池原理》系列从源码角度捋一捋线程池的重点实现。

在此之前先聊聊ThreadPoolExecutor的作者:Doug Lea

Doug Lea

Doug Lea是 java.util.concurrent 包(JUC)的作者,不少网友评价Doug Lea是JDK史上最牛*的程序员,甚至有人感叹:“编程不识Doug Lea,写尽java也枉然”。

接下来盘点ThreadPoolExecutor线程池的基本工作原理和Executors工具类提供的几种常见线程池的效果,简单入个门。

线程池存在的意义

我们知道当下常见的jvm使用的线程都是内核级线程(KLT)。显然,线程是稀缺资源,它的创建与销般是一个相对偏重且耗资源的操作,而Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过度消耗需要设法重用线程执行多个任务

所以线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。

一般什么时候使用线程池?

  • 单个任务处理时间比较短
  • 需要处理的任务数量很大
  • 比较耗时的任务不建议使用线程池,因为容易造成任务的堆积

线程池基本工作原理

ThreadPoolExecutor

jdk提供的线程池实现为ThreadPoolExecutor类(常用就这个类)

ThreadPoolExecutor

其中前3个构造方法都依赖最后一个构造方法,其含义如下

1
2
3
4
5
6
7
8
public ThreadPoolExecutor(int corePoolSize, /// 核心线程数
int maximumPoolSize, /// 最大线程数
long keepAliveTime, /// 非核心线程空闲后的存活时间的数量值
TimeUnit unit, /// 非核心线程空闲后的存活时间的单位
BlockingQueue<Runnable> workQueue, /// 工作队列(阻塞队列)
ThreadFactory threadFactory, /// 线程工厂,主要负责创建线程
RejectedExecutionHandler handler) { /// 拒绝策略
}

拓扑如下

Java 线程池 ThreadPoolExecutor

  • 线程工厂:负责创建线程

  • 工作队列:一个保存任务的阻塞队列(阻塞队列:多线程下保证队列的FIFO特性)

    1
    2
    3
    4
    public interface ThreadFactory {
    // 通过Runnable对象创建线程
    Thread newThread(Runnable r);
    }
  • Worker:线程池实现线程复用以执行任务的核心对象(见下文),自身实现了Runnable接口且内部维护了一个Thread线程对象

  • 核心线程:Worker对象内的线程,线程池常驻线程,成为核心线程的线程不会被销毁

  • 非核心线程:Worker对象内的线程,成为非核心线程的线程进入空闲后,只会存活一定时间(keepAliveTime),之后便正常结束线程。

  • 拒绝策略:任务无法添加时,线程池执行拒绝策略。ThreadPoolExecutor提供四种如图所示的拒绝策略,实现RejectedExecutionHandler接口自身定义拒绝策略。

    1
    2
    3
    public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
    }

线程池的状态

类似线程的状态,线程池也有自己的状态

  • Running:运行状态,接受新任务,也能处理工作队列里的任务

  • Shutdown:关闭状态,不接受新任务,但是处理工作队列里的任务

  • Stop:停止状态,不接受新任务,不处理工作队列里的任务,中断正在处理中的任务

  • Tidying:整理状态,当所有的任务都执行完了,当前线程池已经没有工作线程,这时线程池将会转换为Tidying状态,并且将要调用terminated方法。terminated方法调用完毕之后,线程池进入Terminated状态。

    1
    protected void terminated() { } /// ThreadPoolExecutor terminated方法为空实现,子类可覆盖实现
  • Terminated:终止状态,terminated方法调用完毕之后,线程池进入Terminated状态。

其关系如图(对线程池工作原理有所了解后再回头看此状态转换图会更好)。

线程池添加任务流程

我相信大多数人都比较清楚线程池添加任务的流程,各种博客网图巴拉巴拉,这里再哔哔一下线程池提交任务的流程,如下:

线程池执行任务流程

我们通常通过执行ThreadPoolExecutor实现Executor接口的execute方法(Executor#execute(Runnable))来提交任务,这个流程就是来自该方法的实现!!!

贴一下源码,官方注释给你讲的明明白白,代码也很清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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();
if (workerCountOf(c) < corePoolSize) { /// 1、核心线程是否已经满
if (addWorker(command, true))/// 2、添加worker,以核心线程消费任务(第二个参数为true)
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {/// 3、添加到工作队列是否可行
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))/// 4、添加worker,以非核心线程消费任务(第二个参数为false)
reject(command);/// 5、如果添加失败,说明非核心线程是否已经满,执行拒绝策略
}

Executors工具类提供的常见线程池

可缓存线程池Executors.newCachedThreadPool()

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

分析:核心线程数为0,最大线程数为Integer.MAX_VALUE,工作队列为同步移交队列

新增任务:核心线程数为0 => 任务提交到工作队列 => 工作队列提交时阻塞直到有线程从队列获取任务。

很显然,每个任务都会交由非核心线程来完成。而非核心线程在任务全部执行完毕后只会存在一定时间(60s),所以这是具有缓存效果的线程池(Cached)。

固定线程数量线程池Executors.newFixedThreadPool()

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

分析:核心线程数为固定值,最大线程数与核心线程数相同,工作队列为无界队列

无界队列:队列可存储的元素没有上限,概念上是如此,但实际受数据类型值范围限制,所以无界队列在Java中指可存放元素的最大值为Integer.MAX_VALUE的队列。

LinkedBlockingQueue无参构造方法

1
2
3
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);// 队列元素最大数量为Integer.MAX_VALUE
}

新增任务:当核心线程数已满时,任务都存到工作队列,由于工作队列是无界的,所以队列永远不会满(概念上不会满),所以非核心线程没有存在的必要,即maximumPoolSize - corePoolSize = 0。

很显然,在固定线程数量线程池中,提交的每个任务都会交由核心线程来完成,核心线程数量固定,非核心线程数量为0,线程池常驻线程数量固定,所以称之为拥有固定线程数量的线程池(Fixed)。

单线程线程池Executors.newSingleThreadExecutor()

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

FinalizableDelegatedExecutorService,仅重写了finalize方法,调用父类DelegatedExecutorService线程池shutdown方法。

1
2
3
4
5
6
7
8
static class FinalizableDelegatedExecutorService extends DelegatedExecutorService {
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}
protected void finalize() {
super.shutdown();
}
}

DelegatedExecutorService,该类实现的线程池内部维护了一个实际真正工作的线程池对象,实质是对线程池ExecutorService的一个静态代理。

1
2
3
4
5
6
static class DelegatedExecutorService extends AbstractExecutorService {
private final ExecutorService e;/// 我才是干活的线程池
DelegatedExecutorService(ExecutorService executor) { e = executor; }
public void execute(Runnable command) { e.execute(command); }
// ...
}

分析:阅读源码后,不难发现该种线程池最终工作的线程池还是ThreadPoolExecutor的实例,而ThreadPoolExecutor实例的核心线程数为1,最大线程数也为1,工作队列为无界队列

单线程线程池无非就是数量为1的固定数量线程池(Fixed,corePoolSize = maximumPoolSize = 1),与Fixed线程池不同的是,Single线程池对象在被GC回收时可自动关闭,比较安全,而Fixed线程池的关闭依赖于第三方调用者。

Single线程池更像一个“孤儿”,干活都是同一个Worker,自生自灭。

定时任务线程池

TODO


PS:在下博客里的图绝大部分都是自己通过ProcessOn做的,均带有**@WindShadow**水印,这么认真的开发人员哪里找哦

( ’ - ’ * )