个人主页: 【⭐️个人主页】
需要您的【 点赞+关注】支持
线程的创建、销毁都会带来一定的开销
如果当我们需要使用到多线程时再去创建,使用完又去销毁,这样去使用不仅会拉长业务流程,还会增加创建、销毁线程的开销
于是有了池化技术的思想,将线程提前创建出来,放在一个池子(容器)中进行管理
当需要使用时,从池子里拿取一个线程来执行任务,执行完毕后再放回池子
不仅是线程有池化的思想,连接也有池化的思想,也就是连接池
池化技术
不仅能复用资源
、提高响应
,还方便管理
线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。我们可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。在JAVA中主要是使用ThreadPoolExecutor类来创建线程池,并且JDK中也提供了Executors工厂类来创建线程池(不推荐使用)。
❗❗❗ 这就是线程池最核心的设计思路,「复用线程,平摊线程的创建与销毁的开销代价」。
相比于来一个任务创建一个线程的方式,使用线程池的优势体现在如下几点:
我们先看一张线程池相关接口的类图结构,网上盗来的,但画的还是很全面的
右上角的几个接口可以先不看,等我们介绍到组合任务的时候会继续说的,我们看左边,Executor、ExecutorService 以及 AbstractExecutorService 都是我们熟悉的,它们抽象了任务执行者的基本模型。
ThreadPoolExecutor 是对线程池概念的抽象,它天生实现了任务执行的相关接口,也就是说,线程池也是一个任务的执行者,允许你向其中提交多个任务,线程池将负责分配线程与调度任务。
至于 Schedule 线程池,它是扩展了基础的线程池实现,提供「计划调度」能力,定时调度任务,延时执行等。
Executor
是一套线程池管理框架。是JDK 1.5
中引入的一系列并发库中与Executor
相关的功能类,其中最核心的类就是常见的ThreadPoolExecutor
。
Executor将工作任务
与线程池
进行分离解耦
工作任务被分为两种:
无返回结果的Runnable
和有返回结果的Callable
在线程池中允许执行这两种任务,其中它们都是函数式接口,可以使用lambda
表达式来实现
Future接口用来定义获取异步任务的结果,它的实现类常是FutureTask
线程池在执行Callable任务时,会将使用FutureTask
将其封装成Runnable执行,因此Executor的执行方法入参只有Runnable
FutureTask相当于适配器,将Callable转换为Runnable再进行执行
例子:
@Test
public void testFutureTask() throws ExecutionException, InterruptedException {
FutureTask<String> command = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(5000);
return "hello world";
}
});
result.execute(command);
System.out.println("获取异步结果:start");
String s = command.get();
System.out.println("获取异步结果:" + s);
}
Executor接口
:只有一个execute()方法;
ExecutorService
接口: ExecutorService扩展了Executor接口,增加了生命周期
的管理方法。
ExecutorService的生命周期包括三种状态:运行
、关闭
、终止
。
创建后便进入
运行
状态,当调用了shutdown()方法时,便进入关闭
状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了submit()的任务,当已经提交了的任务执行完后,便到达终止
状态。
ScheduledExecutorService
接口:任务调度的线程池实现,可以在给定的延迟后运行命令或者定期执行命令;
ThreadPoolExecutor
:最核心的线程池实现,用来执行被提交的任务;
RUNNING
(111) :能接受新提交的任务,并且也能处理阻塞队列中的任务;
SHUTDOWN
(000):关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING
状态时,调用 shutdown()
方法会使线程池进入到该状态。(finalize()
方法在执行过程中也会调用shutdown()
方法进入该状态);
STOP
(001):不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING
或 SHUTDOWN
状态时,调用 shutdownNow()
方法会使线程池进入到该状态;
TIDYING
(010):如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated()
方法进入TERMINATED
状态。
TERMINATED
(011):在terminated()
方法执行完后进入该状态,默认terminated()
方法中什么也没有做。
ThreadPoolExecutor 使用int的高三位表示线程池状态,低29位表示线程数量;
什么是“非核心线程”?
核心线程跟创建的先后没有关系,而是跟
工作线程的个数
有关,如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是“非核心线程”,都有被回收的可能。
一个线程执行完了一个任务后,会去阻塞队列里面取新的任务,在取到任务之前它就是一个闲置的线程。
线程回收策略
取任务的方法有两种,一种是通过 take() 方法一直阻塞直到取出任务,另一种是通过 poll(keepAliveTime,timeUnit) 方法在一定时间内取出任务或者超时,如果超时这个线程就会被回收,请注意核心线程一般不会被回收。
那么怎么保证核心线程不会被回收呢?
还是跟
工作线程的个数
有关,每一个线程在取任务的时候,线程池会比较当前的工作线程个数与核心线程数:如果工作线程数小于当前的核心线程数,则使用第一种方法取任务,也就是没有超时回收,这时所有的工作线程都是“核心线程”,他们不会被回收;
如果大于核心线程数,则使用第二种方法取任务,一旦超时就回收,所以并没有绝对的核心线程,只要这个线程没有在存活时间内取到任务去执行就会被回收。
核心线程一般不会被回收,但是也不是绝对的,如果我们设置了允许核心线程超时被回收的话,那么就没有核心线程这种说法了,所有的线程都会通过 poll(keepAliveTime, timeUnit) 来获取任务,一旦超时获取不到任务,就会被回收,一般很少会这样来使用,除非该线程池需要处理的任务非常少,并且频率也不高,不需要将核心线程一直维持着。
非核心线程存活时间
当工作线程数达到 corePoolSize 时,线程池会将新接收到的任务存放在阻塞队列中,而阻塞队列又两种情况:一种是有界的队列,一种是无界的队列。
如果是无界队列,那么当核心线程都在忙的时候,所有新提交的任务都会被存放在该无界队列中,这时最大线程数将变得没有意义,因为阻塞队列不会存在被装满的情况。
如果是有界队列,那么当阻塞队列中装满了等待执行的任务,这时再有新任务提交时,线程池就需要创建新的“临时”线程来处理,相当于增派人手来处理任务。
但是创建的“临时”线程是有存活时间的,不可能让他们一直都存活着,当阻塞队列中的任务被执行完毕,并且又没有那么多新任务被提交时,“临时”线程就需要被回收销毁,在被回收销毁之前等待的这段时间,就是非核心线程的存活时间,也就是 keepAliveTime
属性。
ThreadPoolExecutor 的创建并不复杂,直接 new 就好,只不过构造函数有好久个重载,我们直接看最底层的那个,也就是参数最多的那个。
public ThreadPoolExecutor
( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
参数介绍
创建线程池需要以下几个参数,其中有5个是必需的:
拒绝策略 | 作用 |
---|---|
AbortPolicy 默认 | 抛出异常 |
CallerRunsPolicy | 调用线程来执行任务 |
DiscardPolicy | 不处理,丢弃 |
DiscardOldestPolicy | 丢弃队列中最近一个任务,并立即执行当前任务 |
任务数 <= 核心线程数时,线程池中工作线程 = 任务数
核心线程数 + 队列容量 < 任务数 <= 最大线程数 + 队列容量时,工作线程数 = 任务数 - 队列容量
Executors
已经为我们封装好了 4 种常见的功能线程池,如下:
只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。适用于控制线程最大并发数。
核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。适用于执行定时或周期性的任务。
无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。适用于执行大量、耗时少的任务。
只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。不适合并发以及可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
弊端
:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
主要问题是线程数最大数是 ·Integer.MAX_VALUE·,可能会创建数量非常多的线程,甚至 OOM。
return new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() + 1,
1024,
1, TimeUnit.HOURS,
new ArrayBlockingQueue<>(1024), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("rejected");
}
}
);
线程池帮我们提高对线程的管理和监控能力,本身也降低了因为创建线程和线程切换带来的内存资源和cpu资源的消耗,也提高了整体的响应速度。
但是线程池也需要我们自己通过ThreadPoolExecutor自己定义,拒绝直接适用Executors工具提供的线程池类。导致的线程不可控的问题。