0%

Java中的线程池原理

[TOC]

实现原理

线程池的组成

1.constructs

提交Job的处理流程:

2.new_task

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
// 如果线程数小于核心工作线程数, 则创建线程并执行当前任务
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
// 如线程数大于等于核心工作线程数或线程创建失败, 则将当前任务放到任务队列中.
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command);
}
// 如果线程池不处于运行中或任务无法放入队列, 并且当前线程数量小于最大工作线程数,
// 则创建一个线程执行任务.
else if (!addIfUnderMaximumPoolSize(command))
// 抛出RejectedExecutionException异常
reject(command); // is shutdown or saturated
}
}
  1. 工作线程worker数量低于核心工作线程数corePoolSize时会优先创建一个工作线程worker处理job, 处理成功则返回.
  2. 工作线程worker数量高于核心工作线程数时会优先把job放入到任务队列, 放入队列成功时处理结束.
  3. 入队失败会识别工作线程数是否还小于最大工作线程数maximumPoolsize, 小于的话也会新创建一个工作线程worker处理job.
  4. 饱和策略

此外, 运行过程中, 更新核心工作线程数时, 若发现扩容, 会增加工作线程数.

备注:

  1. Java中的线程与操作系统的线程是一一对应的
  2. 添加新线程需要获得全局锁
    private final ReentrantLock mainLock = new ReentrantLock();, 因此, 当工作线程work数量大于核心工作线程数corePoolSize时, 优先放入任务队列
  3. 只要工作线程达不到corePoolSize, 不管是否线程空闲, 都会创建新线程
  4. 调用prestartAllCoreThreads会初始化所有的核心线程, 没有预热期, 响应快, 但空载浪费资源
  5. 任务队列添加任务, 不需要获取全局锁, 效率高
  6. 工作线程的复用: 执行完一个任务, 不断从任务队列取任务, 避免因创建和销毁操作系统线程带来的性能消耗
  7. 获取全局锁, 是性能性能瓶颈, corePoolSize是预热

工作线程的销毁

满足下面条件会销毁:

  1. 任务队列里没有job并且工作线程worker数量超过了核心工作线程数corePoolSize.
  2. 任务队列里没有job并且允许工作线程数量小于核心工作线程参数为true, 此场景会至少保留一个工作线程线程.

工作线程空闲后, 最长等待keepAliveTime

ThreadPoolExecutor

1
2
3
4
5
6
7
8
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

用给定的初始参数和默认的线程工厂及饱和策略创建新的 ThreadPoolExecutor.
使用 Executors 工厂方法之一比使用此通用构造方法方便得多, 但是阿里巴巴Java规约不推荐使用, 见下文

  • 参数:
    • corePoolSize - 池中所保存的线程数, 包括空闲线程.
    • maximumPoolSize - 池中允许的最大线程数.
    • keepAliveTime - 当线程数大于核心时, 此为终止前多余的空闲线程等待新任务的最长时间.
    • unit - keepAliveTime 参数的时间单位.
    • workQueue - 执行前用于保持任务的队列. 此队列仅保持由 execute 方法提交的 Runnable 任务.
  • 抛出:
    • IllegalArgumentException - 如果 corePoolSizekeepAliveTime 小于零, 或者 maximumPoolSize 小于或等于零, 或者 corePoolSize 大于 maximumPoolSize.
    • NullPointerException - 如果 workQueuenull

工作线程的存储

使用了HashSet来存储工作线程worker, 通过可重入锁ReentrantLock对其进行并发保护. 每个worker都是一个Runnable接口.

1
2
3
4
5
/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();

任务队列 runnableTaskQueue

线程池中的队列采用的是BlockingQueue

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列, 此队列按 FIFO(先进先出)原则对元素进行排序.
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列, 此队列按FIFO排序元素, 吞吐量通常要高于ArrayBlockingQueue. 静态工厂方法Executors.newFixedThreadPool()使用了这个队列.
  • SynchronousQueue:一个不存储元素的阻塞队列. 每个插入操作必须等到另一个线程调用移除操作, 否则插入操作一直处于阻塞状态, 吞吐量通常要高于Linked-BlockingQueue, 静态工厂方法Executors.newCachedThreadPool使用了这个队列.
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列.

线程工厂 ThreadFactory

设置创建线程的工厂, 通过线程工厂给每个创建出来的线程设置更有意义的名字. 使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字, 代码如下.

1
newThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

RejectedExecutionHandler(饱和策略)

当队列和线程池都满了, 说明线程池处于饱和状态, 那么必须采取一种策略处理提交的新任务. 这个策略默认情况下是 AbortPolicy , 表示无法 处理新任务时抛出异常. 在JDK 1.5中Java线程池框架提供了以下4种策略.

  • AbortPolicy :直接抛出异常.

  • CallerRunsPolicy :只用调用者所在线程来运行任务.

  • DiscardOldestPolicy :丢弃队列里最近的一个任务, 并执行当前任务.

  • DiscardPolicy :不处理, 丢弃掉.

当然, 也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略. 如记录日志或持久化存储不能处理的任务.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 线程池异常处理类:
* 任务执行失败, 持久化到数据库
* @author averyzhang
*/
public class MyRejectedExecutionHandler implements RejectedExecutionHandler {

@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
System.out.println("Begin exception handler-----------");
//执行失败任务
CachingOnFirstOpenRunnable coor = (CachingOnFirstOpenJob)task;
List<CachingEntity> list = coor.getLastData();
CachingDaoUtils.save(list);
//打印线程池的对象
System.out.println("The pool RejectedExecutionHandler = "+executor.toString());
}
}

提交任务: submit与execute

execute 没有返回值

submit 可以通过 Future 接口, 获取任务执行的结果: 线程池会返回一个future类型的对象, 通过这个 future对象可以判断任务是否执行成功, 并且可以通过future的get()方法来获取返回值, get()方法会阻塞当前线程直到任务完成, 而使用get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回, 这时候有可能任务没有执行完.

关闭: shutdown与shutdownNow

原理是遍历线程池中的工作线程, 然后逐个调用线程的interrupt方法来中断线程, 所以无法响应中断的任务可能永远无法终止.

  • shutdownNow首先将线程池的状态设置成 STOP, 然后尝试停止所有的正在执行或暂停任务的线程, 并返回等待执行任务的列表
  • shutdown只是将线程池的状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程

获取线程的关闭状态:

调用两个关闭方法, isShutdown方法就会返回true. 当所有的任务都已关闭后, 才表示线程池关闭成功, 这时调用isTerminaed方法会返回true.

至于应该调用哪一种方法来关闭线程池, 应该由提交到线程池的任务特性决定, 通常调用shutdown方法来关闭线程池, 如果任务不一定要执行完, 则可以调用shutdownNow方法.

优化配置

性质不同的任务可以用不同规模的线程池分开处理:

  • CPU密集型任务应配置尽可能小的线程, 如配置Ncpu+1个线程的线程池
  • IO密集型任务线程并不是一直在执行任务, 则应配置尽可能多的线程, 如2*Ncpu
  • 混合型的任务, 如果可以拆分, 将其拆分成一个CPU密集型任务和一个IO密集型任务, 只要这两个任务执行的时间相差不是太大, 那么分解后执行的吞吐量将高于串行执行的吞吐量. 如果这两个任务执行时间相差太大, 则没必要进行分解.
  • 可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
  • 如果任务很多, 并且每个任务执行的时间比较短, 可以调大keepAliveTime时间, 提高线程的利用率.
  • 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理. 它可以让优先级高的任务先执行. 优先级低的任务可能永远不能执行.
  • 建议使用有界队列. 有界队列能增加系统的稳定性和预警能力, 可以根据需要设大一点:如果采用无解队列, 当任务无法处理引起堆积, 系统撑爆, 殃及其他业务

通常基于几个维度进行:待处理工作job数、线程池定义的最大最小工作线程数、工作线程闲置时间.

监控

通过线程池提供的参数进行监控, 在监控线程池的时候可以使用以下属性.

  • taskCount:线程池需要执行的任务数量.

  • completedTaskCount:线程池在运行过程中已完成的任务数量, 小于或等于taskCount.

  • largestPoolSize:线程池里曾经创建过的最大线程数量. 通过这个数据可以知道线程池是否曾经满过. 如该数值等于线程池的最大大小, 则表示线程池曾经满过.

  • getPoolSize:线程池的线程数量. 如果线程池不销毁的话, 线程池里的线程不会自动销 毁, 所以这个大小只增不减.

  • getActiveCount:获取活动的线程数. 通过扩展线程池进行监控.

  • 通过继承线程池来自定义线程池, 重写线程池的 beforeExecuteafterExecuteterminated方法, 可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控. 例如, 监控任务的平均执行时间、最大执行时间和最小执行时间等. 这几个方法在线程池里是空方法.

1
2
3
protected void beforeExecute(Thread t,Runnable r) {

}

sample

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
40
41
42
public class MyScheduledExecutorService extends ScheduledThreadPoolExecutor {
private static final Logger logger = LoggerFactory.getLogger(MyScheduledExecutorService.class);

public TableauScheduledExecutorService(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, threadFactory, handler);
}

public TableauScheduledExecutorService(int corePoolSize, ThreadFactory threadFactory) {
super(corePoolSize, threadFactory);
}

@Override
protected void beforeExecute(Thread t, Runnable r) {
logger.info("############# before execute \n" + currentStatus());
}

@Override
protected void afterExecute(Runnable r, Throwable t) {
logger.info("############# after execute \n" + currentStatus());
}

@Override
protected void terminated() {
logger.info("############# terminated \n" + currentStatus());
}

private String currentStatus() {
// 需要执行的任务数目
final long taskCount = getTaskCount();
// 执行完成的任务数目
final long completedTaskCount = getCompletedTaskCount();
// 线程池最大线程数量
final int largestPoolSize = getLargestPoolSize();
// 线程池线程数量
final int poolSize = getPoolSize();
// 活动线程数量
final int activeCount = getActiveCount();

return "[要执行任务:" + taskCount + ",完成:" + completedTaskCount + ",线程池最大数量:" + largestPoolSize +
",当前线程数量:" + poolSize + ",活动线程数量:" + activeCount + "]";
}
}

TODO 等待关闭

Alibaba Java 规约

线程池不允许使用Executors去创建, 而是通过ThreadPoolExecutor的方式, 这样的处理方式让写的同学更加明确线程池的运行规则, 规避资源耗尽的风险. 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存, 甚至OOM.
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE, 可能会创建数量非常多的线程, 甚至OOM.

Positive example 1:

1
2
//org.apache.commons.lang3.concurrent.BasicThreadFactory
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

Positive example 2:

1
2
3
4
5
6
7
8
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build();

//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

pool.execute(()-> System.out.println(Thread.currentThread().getName()));
pool.shutdown();//gracefully shutdown

Positive example 3:

1
2
3
4
5
6
7
8
9
10
11
<bean id="userThreadPool"
class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="10" />
<property name="maxPoolSize" value="100" />
<property name="queueCapacity" value="2000" />

<property name="threadFactory" value= threadFactory />
<property name="rejectedExecutionHandler">
<ref local="rejectedExecutionHandler" />
</property>
</bean>
1
2
//in code
userThreadPool.execute(thread);

线程池的关闭

等待关闭超时

next…TODO

  1. Future接口
  2. 任务队列的实现
  3. ArrayBlockingQueue
  4. LinkedBlockingQueue
  5. SynchronousQueue
  6. PriorityBlockingQueue
  7. 线程池的底层实现
  8. guava与线程工厂的使用
  9. 线程关闭与中断: 强制关闭与响应中断
  10. Java获取设备参数的API
  11. 连接池