Java线程池简单了解


Java线程池简单了解

[TOC]

主要查看文章:

Java线程池实现原理及其在美团业务中的实践

JavaGuide -线程池最佳实践

新手也能看懂的线程池学习总结

1.为什么要用线程池

池化思想,比如线程池,数据库连接池,HTTP连接池等。主要是为了能够重复利用资源,提高资源的利用率。

线程池的好处:

  • 降低资源消耗:可重复利用资源
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行
  • 提高线程可管理性:防止线程创建过多消耗尽系统资源内存等

线程池解决的核心问题:资源管理问题

2 Java线程池使用例子

2.1 总览

Executor使用示意图

  1. 主线程首先创建实现 Runnable 或者 Callable 接口的任务对象

  2. 把创建完成的实现 Runnable/Callable接口的对象,直接交给 ExecutorService 执行:

    • ExecutorService.execute(Runnable command)
    • ExecutorService.submit(Runnable task)
    • ExecutorService.submit(Callable <T> task)

    关于 submit()execute() 区别看下面。

  3. 如果使用的submit提交,则会返回Future对象,包含执行结果。

    FutureTask = Future + Runnable,可以用它来直接提交任务与获得返回结果。

  4. 主线程future.get()获取返回结果,或者主线程 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行,参数boolean表示是否让任务完成。

submit与execute

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否

  2. submit()方法用于提交需要返回值的任务

    线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功

    • 可以通过 Futureget()方法来获取返回值get()方法会阻塞当前线程直到任务完成

    • 而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

2.2 ThreadPoolExecutor实例

==Java中的线程池:ThreadPoolExecutor==

ThreadPoolExecutor的构造函数,用它来创建线程池:

1
2
3
4
5
6
7
8
9
10
11
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
)

这几个参数,稍微记一下:

  • 核心线程数量
  • 最大线程数量
  • 存活时间(当前线程数 > 核心线程数,多余线程的最长存活时间)
  • 时间单位(TimeUnit.SECONDS)
  • 任务队列
  • 线程工厂,一般默认不用管
  • 拒绝策略(任务过多时,定制策略处理任务)

创建例子:

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
43
44
/** 核心线程数 **/
private static final int CORE_POOL_SIZE = 5;
/** 最大线程数 **/
private static final int MAX_POOL_SIZE = 10;
/** 队列容量 **/
private static final int QUEUE_CAPACITY = 100;
/** 最大存活时间 **/
private static final Long KEEP_ALIVE_TIME = 1L;

public static void main(String[] args) {

//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数,自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());

// 循环,线程池中,创建10个线程
for (int i = 0; i < 10; i++) {
// Lambda表达式,调用execute的参数类型Runnable接口的构造函数,返回匿名对象并实现其中方法(大括号中)
// 相当于new Runnable(){ run(){} }
executor.execute(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("CurrentThread name:" + Thread.currentThread().getName() + ";date:" + Instant.now());
});
}
//终止线程池
executor.shutdown();
try {
// 等待所有任务完成
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Finished all threads");
}
  1. ThreadPoolExecutor构造函数创建线程池
  2. 线程池中运行线程 executor.execute(Runnable r)
  3. 终止线程池 exector.shutdown()

其中,关于Lamda表达式->:Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

下面这个例子就是调用了一个构造函数,返回了一个新建的对象,然后传入函数参数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
EventQueue.invokeLater(() -> {
JFrame frame = new ImageViewerFrame();
frame.setTitle("ImageViewer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});

EventQueue.invokeLater(new Runnable() {
public void run() {
JFrame frame = new ImageViewerFrame();
frame.setTitle("ImageViewer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});

3.线程池核心设计与实现总览

ThreadPoolExecutor的UML类图

这个UML看的不是很懂。。。继续往下看

ThreadPoolExecutor运行流程

4. 线程池生命周期

其中,线程池 ThreadPoolExecutor的状态有5种:

ThreadPoolExecutor状态5种

其状态转移图:

5种状态的转移

5. 任务执行机制

线程池的本质是对任务和线程的管理,而做到这一点==最关键的思想就是将任务和线程两者解耦==,不让两者直接关联,才可以做后续的分配工作。

线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。

阻塞队列缓存任务,工作线程(消费者)从阻塞队列中获取任务。

分为以下几个模块:

  • 任务调度
  • 任务缓冲
  • 任务申请
  • 任务拒绝

5.1 任务调度

一个任务提交到线程池了之后,会经过一下判断

image-20210526225421822

5.2 任务缓冲

任务缓冲模块是线程池能够管理任务的核心部分

线程池的本质是对任务和线程的管理,而做到这一点==最关键的思想就是将任务和线程两者解耦==,不让两者直接关联,才可以做后续的分配工作。

线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。

阻塞队列缓存任务,工作线程(消费者)从阻塞队列中获取任务。

阻塞队列示意

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:

  • 在队列为空时,获取元素的线程会等待队列变为非空。
  • 当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景:

  • 生产者是往队列里添加元素的线程
  • 消费者是从队列里拿元素的线程

阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

阻塞队列成员

5.3 任务申请

任务的执行有两种可能:

  • 一种是任务直接由新创建的线程执行(仅出现在线程初始创建的时候)
  • 另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行(线程获取任务绝大多数的情况)

线程池中的线程申请任务的步骤:

线程申请任务步骤

申请任务时,通过一个 getTask()去执行,经过以上判断的目的是为了防止线程池中的线程过多,控制线程数量

5.4 任务拒绝

任务拒绝模块是线程池的==保护部分==。

线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

RejectedExecutionHandler 是一个接口,可以自定义拒绝策略。

也可以用JDK自带的4种拒绝策略:

JDK自带的4种拒绝策略

6.使用场景举例

场景1:快速响应用户请求

并行执行任务提升任务响应速度

描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

这种场景最重要的就是获取最大的响应速度去满足用户,

所以应该不设置队列去缓冲并发任务调高corePoolSize和maxPoolSize尽可能创造多的线程快速执行任务。

场景2:快速处理批量任务

并行执行任务提升批量任务执行速度

描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。

设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数

设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

7. 几个对比

  • Runnable 与 Callable:前者不抛异常/返回结果,后者会
  • execute() 与 submit():前者不会返回结果,后者会
  • shutdown() 与 shutdownNow():前者会等待队列中的任务执行完毕,后者不会
  • isShutdown() 与 isTeminated() :前者是shutdown()了之后就true,后者是等全部执行完成了之后才true

总结

如何回答线程池原理:

感觉只要答出,任务–线程解耦,以及阻塞队列,生产者消费者模式,就可以了,这几个是重点。


文章作者: SongX64
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 SongX64 !
  目录