Java线程池简单了解
[TOC]
主要查看文章:
1.为什么要用线程池
池化思想,比如线程池,数据库连接池,HTTP连接池等。主要是为了能够重复利用资源,提高资源的利用率。
线程池的好处:
- 降低资源消耗:可重复利用资源
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行
- 提高线程可管理性:防止线程创建过多消耗尽系统资源内存等
线程池解决的核心问题:资源管理问题
2 Java线程池使用例子
2.1 总览
主线程首先创建实现
Runnable
或者Callable
接口的任务对象把创建完成的实现
Runnable
/Callable
接口的对象,直接交给ExecutorService
执行:ExecutorService.execute(Runnable command)
ExecutorService.submit(Runnable task)
ExecutorService.submit(Callable <T> task)
关于
submit()
与execute()
区别看下面。如果使用的submit提交,则会返回Future对象,包含执行结果。
FutureTask = Future + Runnable
,可以用它来直接提交任务与获得返回结果。主线程
future.get()
获取返回结果,或者主线程FutureTask.cancel(boolean mayInterruptIfRunning)
来取消此任务的执行,参数boolean表示是否让任务完成。
submit与execute
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()
方法用于提交需要返回值的任务。线程池会返回一个
Future
类型的对象,通过这个Future
对象可以判断任务是否执行成功可以通过
Future
的get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成;而使用
get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
2.2 ThreadPoolExecutor实例
==Java中的线程池:ThreadPoolExecutor==
ThreadPoolExecutor的构造函数,用它来创建线程池:
1 | /** |
这几个参数,稍微记一下:
- 核心线程数量
- 最大线程数量
- 存活时间(当前线程数 > 核心线程数,多余线程的最长存活时间)
- 时间单位(TimeUnit.SECONDS)
- 任务队列
- 线程工厂,一般默认不用管
- 拒绝策略(任务过多时,定制策略处理任务)
创建例子:
1 | /** 核心线程数 **/ |
ThreadPoolExecutor
构造函数创建线程池- 线程池中运行线程
executor.execute(Runnable r)
- 终止线程池
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.线程池核心设计与实现总览
这个UML看的不是很懂。。。继续往下看
4. 线程池生命周期
其中,线程池 ThreadPoolExecutor
的状态有5种:
其状态转移图:
5. 任务执行机制
线程池的本质是对任务和线程的管理,而做到这一点==最关键的思想就是将任务和线程两者解耦==,不让两者直接关联,才可以做后续的分配工作。
线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。
阻塞队列缓存任务,工作线程(消费者)从阻塞队列中获取任务。
分为以下几个模块:
- 任务调度
- 任务缓冲
- 任务申请
- 任务拒绝
5.1 任务调度
一个任务提交到线程池了之后,会经过一下判断
5.2 任务缓冲
任务缓冲模块是线程池能够管理任务的核心部分。
线程池的本质是对任务和线程的管理,而做到这一点==最关键的思想就是将任务和线程两者解耦==,不让两者直接关联,才可以做后续的分配工作。
线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。
阻塞队列缓存任务,工作线程(消费者)从阻塞队列中获取任务。
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:
- 在队列为空时,获取元素的线程会等待队列变为非空。
- 当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景:
- 生产者是往队列里添加元素的线程
- 消费者是从队列里拿元素的线程
阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
5.3 任务申请
任务的执行有两种可能:
- 一种是任务直接由新创建的线程执行(仅出现在线程初始创建的时候)
- 另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行(线程获取任务绝大多数的情况)
线程池中的线程去申请任务的步骤:
申请任务时,通过一个 getTask()
去执行,经过以上判断的目的是为了防止线程池中的线程过多,控制线程数量。
5.4 任务拒绝
任务拒绝模块是线程池的==保护部分==。
线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
RejectedExecutionHandler
是一个接口,可以自定义拒绝策略。
也可以用JDK自带的4种拒绝策略:
6.使用场景举例
场景1:快速响应用户请求
描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。
这种场景最重要的就是获取最大的响应速度去满足用户,
所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
场景2:快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。
这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。
设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数
设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
7. 几个对比
- Runnable 与 Callable:前者不抛异常/返回结果,后者会
- execute() 与 submit():前者不会返回结果,后者会
- shutdown() 与 shutdownNow():前者会等待队列中的任务执行完毕,后者不会
- isShutdown() 与 isTeminated() :前者是shutdown()了之后就true,后者是等全部执行完成了之后才true
总结
如何回答线程池原理:
感觉只要答出,任务–线程解耦,以及阻塞队列,生产者消费者模式,就可以了,这几个是重点。