文章目录
什么是线程池?
-
举个例子,我们在操作数据库时需要先跟数据库建立连接,拿到数据库连接之后才能操作数据库,当操作完数据库后将连接关闭。创建和销毁都是比较耗时间的,但真正跟业务相关的操作都是耗时比较短的。
每进行一个数据库操作时,都会创建连接、用完销毁,为了提升效率,后面出现了数据库连接池 。系统启动的时候创建出很多的数据库连接放到池子里,使用时直接从池子里获取一个连接,使用完成后将连接放回到池子中,中间省去了创建和销毁的时间,这样提高了整体的性能。 -
线程池和数据库连接池的类似,使用线程去处理任务时,需要先创建线程,可能创建线程的时间比处理业务的时间还要长。如果能同数据库连接池一样,提前创建好线程,这样在使用时,直接从池子里获取,使用完不销毁线程,而是放回池子里,这样就节省了创建和销毁的时间。同事也提高了效率。
总结:线程池能够提高响应速度、加速处理任务、降低线程创建销毁需要的系统资源 。同时线程池也能更好的管理并发线程的数量。
线程池应用场景?线程池的优点?
-
加快请求响应(提高响应速度)
比如用户在饿了么上查看某商家外卖,需要聚合商品库存、店家、价格、红包优惠等等信息返回给用户,接口逻辑涉及到聚合、级联等查询,从这个角度来看接口返回越快越好,那么就可以使用多线程方式,把聚合/级联查询等任务采用并行方式执行,从而缩短接口响应时间。这种场景下使用线程池的目的就是为了缩短响应时间,往往不去设置队列去缓冲并发的请求,而是会适当调高corePoolSize和maxPoolSize去尽可能的创造线程来执行任务。 -
加速处理大量任务(提高吞吐量)
比如业务中台每10分钟就调用接口统计每个系统/项目的PV/UV等指标然后写入多个sheet页中返回,这种情况下往往也会使用多线程方式来并行统计。和"时间优先"场景不同,这种场景的关注点不在于尽可能快的返回,而是关注利用有限的资源尽可能的在单位时间内处理更多的任务,即吞吐量优先。这种场景下我们往往会设置队列来缓冲并发任务,并且设置合理的corePoolSize和maxPoolSize参数,这个时候如果设置了太大的corePoolSize和maxPoolSize可能还会因为线程上下文频繁切换降低任务处理速度,从而导致吞吐量降低。以上两种使用场景和JVM里的
ParallelScavenge
和CMS垃圾收集器
有较大的类比性,ParallelScavenge垃圾收集器关注点在于达到可观的吞吐量,而CMS垃圾收集器重点关注尽可能缩短GC停顿时间。 -
优点:
- 降低资源消耗:通过重复利用已创建的线程来降低线程创建和线程销毁的资源消耗。
- 提高响应速度:执行任务时,不需要再等待线程创建完成就能直接获取线程立即执行。
- 更好的管理线程:线程是系统的稀缺资源,无限的创建线程不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以对线程进行统一管理,调优和监控。
线程池怎么创建?
线程池可以自动创建,也可以手动创建。自动创建体现在Executors工具类中,常见的有:
newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool
;手动创建可以更灵活的控制线程池的各项参数,体现在代码中即ThreadPoolExecutor类构造器上
ThreadPoolExecutor 通过 直接new()方式创建,包含七个参数
Executors 通过 类名.newXxxThreadXxx()方法获取一个线程池实例
①手动建线程池:
-
ThreadPoolExecutor():最原始的创建线程池的⽅式,它包含了 7 个参数可供设置。
构造函数:public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { //下方都是校验,主要看参数 if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
参数:
- corePoolSize:核心线程大小(数量)。当提交了一个任务到线程池中,即使当前线程池中有空余的线程可以处理任务,但还是会创建一个新的线程来执行任务,等到工作线程数达到核心线程大小(数量)时,就不创建新的线程了。
如果调用了线程池的prestartAllCoreThreads方法
,线程池会提前按照核心线程数量将线程创建好,并启动。 - maximumPoolSize:线程池允许创建的最大线程数量。通过构造函数中的判断可以得知,该值必须大于或等于1。已创建的线程数量小于 maximumPoolSize时,会创建新的线程执行任务。注意:如果使用了
无界队列
,那么不管有多少任务,都会加入队列,这个参数就不起作用了。
说明:无界队列,没有大小限制(默认是Integer.MAX_VALUE)的队列。 有界队列,有固定大小限制。
- keepAliveTime:空闲线程的存活时间。当线程数量大于corePoolSize时,并且存活时间达到keepAliveTime时间,空闲的线程会被销毁直到只剩下corePoolSize数量为止。如果任务很多,并且每个任务执行时间较短,避免线程重复创建和回收,可以调大存活时间,提高线程利用率。
- unit:keepAliveTime时间的单位。常用的枚举类
java.util.concurrent.TimeUnit
。TimeUnit.SECONDS
- workQueue:任务队列。被提交但是未被执行的任务,用于缓存待处理任务的阻塞队列。
eg:
new ArrayBlockingQueue<>(1)
- threadFactory:生成线程池中工作线程的线程工厂。用于创建线程,一般默认的即可,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
eg:
Executors.defaultThreadFactory();//默认 Executors.privilegedThreadFactory();
- handle:拒绝策略。当前队列已满,并且工作线程数量大于maximumPoolSize时,如何拒绝新来的线程请求的策略。
说明:工作队列中已经放不下任务了,并且已达到最大可创建的线程数量时。新来的任务不会放入队列中,而是会按照拒绝策略处理。
eg:
(r, executors) -> { System.out.println("被拒绝策略的任务:" + r.toString()); })
- corePoolSize:核心线程大小(数量)。当提交了一个任务到线程池中,即使当前线程池中有空余的线程可以处理任务,但还是会创建一个新的线程来执行任务,等到工作线程数达到核心线程大小(数量)时,就不创建新的线程了。
②调用工具类创建:
- Executors.newFixedThreadPool(int corePoolSize)
特点:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
构造方法:newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue执行长期任务性能好,有固定线程数的线程池。
说明:corePoolSize、maximumPoolSize都是ThreadPoolExecutor的参数 - Executors.newCachedThreadPool()
特点:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;总结:可扩容,遇强则强 - Executors.newSingleThreadExecutor()
特点:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;一池一线程 - Executors.newScheduledThreadPool(int corePoolSize)
特点:创建⼀个可以执⾏延迟任务的线程池; - Executors.newSingleThreadScheduledExecutor()
特点:创建⼀个单线程的可以执⾏延迟任务的线程池; - Executors.newWorkStealingPool(int parallelism)
特点:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
线程池的关闭
线程池中提供了两种关闭方法,作用不同
-
shutdown():当线程池调用shutdown时,线程池拒绝接受新提交的任务,内部会将队列中等待执行&&进行中的任务执行完成后,线程销毁,线程池关闭。
-
shutdownNow():当线程池调用了shutdownNow时,会将队列中等待处理的任务清除,内部会将进行中的任务执行完成后,线程销毁,线程池关闭。
当调用者两个方法之后,线程池会遍历内部的工作线程,然后调用每个工作线程的
interrrupt
方法给线程发送中断信号,内部如果无法响应中断信号的可能永远无法终止,所以如果内部有无限循环的,最好在循环内部检测一下线程的中断信号,合理的退出。调用者两个方法中任意一个,线程池的isShutdown
方法就会返回true
,当所有的任务线程都关闭之后,才表示线程池关闭成功,这时调用isTerminaed
方法会返回true
。
线程池的工作流程?线程的创建流程?:
举个栗子:现有一个线程池,corePoolSize=10,maximumPoolSize=20,队列长度为100。线程池在执行任务时会先创建出10个核心线程数。接下来新提交的任务会进入到队列中,一直到队列放满,此时会创建出对于的线程来执行任务(最多20个),如果这时还有新提交的任务就会执行拒绝策略。
用不说人话的方式说工作流程(此过程为默认顺序,也可以手动控制):
- ①当任务提交的时候,会先判断当前线程池中的线程数量是否达到核心线程数(corePoolSize)
- ②如果没达到核心线程数量(corePoolSize),则会继续创建新的线程来执行任务,即使现在已有空闲的线程。注意:如果调用了
prestartAllCoreThreads()
方法,线程池则会提前创建好核心线程数量(corePoolSize)的线程。 - ③如果已达到核心线程数量(corePoolSize),则会判断阻塞队列(workQueue)是否已满。
- ④阻塞队列未满,则会放入阻塞队列中进行等待,等待被线程执行。
- ⑤阻塞队列已满,此时会判断线程数量是否到达最大线程数(maximumPoolSize)。
- ⑥阻塞队列已满
并且
未达到最大线程数量,则会创建新的线程去执行任务。 - ⑦阻塞队列已满
并且
已达到最大线程数量,则会执行拒绝策略来处理新来的任务。
- ⑥阻塞队列已满
- ②如果没达到核心线程数量(corePoolSize),则会继续创建新的线程来执行任务,即使现在已有空闲的线程。注意:如果调用了
在任务不断增加的过程中,线程池会逐一进行下面四个参数的判断:
核心线程数(corePoolSize)
阻塞队列、工作队列(workQueue)
最大线程数(maximumPoolSize)
拒绝策略
workQueue队列(没深扣,浅看了一下)
SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务
handle 拒绝策略(没深扣,浅看了一下)
AbortPolicy:中断抛出异常
DiscardPolicy:默默丢弃任务,不进行任何通知
DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)
线程池怎么实现线程复用的?(知道就行)
在线程池中同一个线程执行不同的任务。
假设现在有 100 个任务,我们创建一个固定线程的线程池(FixedThreadPool),核心线程数和最大线程数都是 3,那么当这个 100 个任务执行完,都只会使用三个线程。
例子:
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < 100; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ "办理业务");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
threadPool.shutdown();
}catch (Exception e){
e.printStackTrace();
}
}
输出结果:
pool-1-thread-1办理业务
pool-1-thread-3办理业务
pool-1-thread-2办理业务
pool-1-thread-1办理业务
pool-1-thread-3办理业务
pool-1-thread-2办理业务
pool-1-thread-1办理业务
pool-1-thread-3办理业务
pool-1-thread-2办理业务
线程复用原理:
-
通过Thread创建的线程。一个线程必须对应一个任务。这样的话每一个线程只能执行一个任务。
而线程池将线程和任务进行了解耦,线程是线程,任务是任务。同一个线程可以在阻塞队列中不断获取新的任务执行。其核心原理在于对Thread进行了封装,并不是在每一次执行任务时调用Thread.start()方法来创建新线程,而是让每一个线程去执行“循环任务”,在“循环任务”之中再去检查是否有需要被执行的任务,如果有直接调用run方法,将run方法当做一个普通的方法去执行。通过这种方式可以将单个固定的线程跟所有的任务串联起来。 -
另一个版本的说法:
线程采用的是生产者消费者模型,也就是一个贤臣对应一个人物。
生产者和消费者通过中间容器(线程池)进行解耦。生产者负责提交任务给中间容器,消费者负责从中间容器中获取任务执行。
Callable 和 Runnable
Callable和Runnable都可以理解为是任务。任务逻辑封装在run
方法内。Callable和Runnable的区别在于Runnable 没有返回值,并且不能抛出异常throws
(可以try cache)。Callable可以有返回值并且可以抛出异常和try cache。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
小栗子:
public class ThreadPoolDemo4 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
for (int i = 0; i < 20; i++) {
//提交任务 并 打印执行结果
Future<String> futureResult = threadPool.submit(new Task());
System.out.println(futureResult.get());
}
threadPool.shutdown();
}catch (Exception e){
e.printStackTrace();
}
}
}
class Task implements Callable<String>{
@Override
public String call() throws Exception {
int idx = new Random().nextInt(4) + 1;
String result = "";
switch (idx) {
case 1: {result = Thread.currentThread().getName()+"号线程,执行结果为:腊八蒜";break;}
case 2: {result = Thread.currentThread().getName()+"号线程,执行结果为:糖蒜";break;}
case 3: {result = Thread.currentThread().getName()+"号线程,执行结果为:生蒜";break;}
default: {result = Thread.currentThread().getName()+"号线程,执行结果为:大脑瓜崩,吃什么吃!";break;}
}
return result;
}
}
执行结果:
pool-1-thread-1号线程,执行结果为:大脑瓜崩,吃什么吃!
pool-1-thread-2号线程,执行结果为:糖蒜
pool-1-thread-3号线程,执行结果为:大脑瓜崩,吃什么吃!
pool-1-thread-4号线程,执行结果为:糖蒜
pool-1-thread-5号线程,执行结果为:糖蒜
pool-1-thread-1号线程,执行结果为:腊八蒜
pool-1-thread-2号线程,执行结果为:糖蒜
pool-1-thread-3号线程,执行结果为:腊八蒜
pool-1-thread-4号线程,执行结果为:腊八蒜
pool-1-thread-5号线程,执行结果为:生蒜
Future 和 FutureTask
FutureTask实现了Future接口。
Future接口用来存储异步任务的执行结果。当线程池提交了一个Callable任务时会返回Future对象,可以通过get()方法获取到任务的执行结果。过程:主线程提交(submit())了一个任务给子线程,子线程去执行任务,执行完成后主线程获取(get())任务结果。
Future接口常用方法:
- get():获取任务结果。若任务未执行完成则会一直阻塞,方法抛出 ExecutionException异常。
- get(long timeout, TimeUnit unit):设置一个时间,用来获取执行结果。超过时间范围会抛出 TimeoutException 异常。如果超时,则需要手动取消任务 cancel。
- cancel(boolean mayInterruptIfRunning):取消执行任务。传入参数用来分别是否立即中断正在执行的任务。
- isDone():判断任务是否执行完毕。执行完毕不保证是成功执行。仅仅是一种状态表示这个任务已完结,可能任务中间的过程中出现异常、或是被中断但都执行结束。表示这个任务结束不会再执行。
- isCancelled()判断任务是否被取消
/**
* future
*/
ExecutorService service = Executors.newFixedThreadPool(1);
Future<String> future = service.submit(new Callable<String>() {
@Override
public String call(){
return Thread.currentThread().getName()+"执行结果……";
}
});
try {
System.out.println("get():" + future.get());
System.out.println("get(long timeout, TimeUnit unit):" + future.get(3,TimeUnit.SECONDS));
System.out.println("isDone():" + future.isDone());
}catch (Exception e3){
e3.printStackTrace();
}
/**
* futureTask
*/
FutureTask<String> stringFutureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {return "执行结果";}
});
service.execute(stringFutureTask);
try {
System.out.println(stringFutureTask.get());
}catch (Exception e3){
e3.printStackTrace();
}
线程池优化(先不钻牛角尖了,等后面在研究吧……)
后面再研究。
线程池扩展方法(beforeExecutor()、afterExecutor()、terminated())
- beforeExecutor(Thread t, Runnable r):在执行任务之前调用方法,有两个参数第一个执行任务的线程,第二个是任务。
- afterExecutor(Runnable r, Throwable t):任务执行完成之后调用的方法,2个参数,第1个参数表示任务,第2个参数表示任务执行时的异常信息,如果无异常,第二个参数为null
- terminated():线程池最终关闭之后调用的方法。所有的工作线程都退出了,最终线程池会退出,退出时调用该方法
eg:
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,
5,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
Executors.defaultThreadFactory(),
(r,executors)->{//拒绝策略
System.out.println("无法处理的任务:" + r.toString());
}){
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("在任务执行前执行 beforeExecute(); t:" + t.getName() + ";r:" + r.toString());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
if(t!=null)
System.out.println("任务执行完毕后执行 afterExecute(); r:" + r.toString() + ";t:" + t.toString());
System.out.println("任务出现异常&&执行完毕后执行 afterExecute(); r:" + r.toString());
}
@Override
protected void terminated() {
System.out.println("线程池关闭后执行 terminated();");
}
};
try {
for (int i = 0; i < 5; i++) {
Future<Object> futureResult = executor.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
return Thread.currentThread().getName() + "号线程开始执行任务====================================!";
}
});
try {
System.out.println(futureResult.get());
}catch (Exception e){
e.printStackTrace();
}
}
executor.shutdown();
}catch (Exception e){
e.printStackTrace();
}
}
//输出结果
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@5bc2a3fc
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@5bc2a3fc
pool-1-thread-1号线程开始执行任务====================================!
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@550546f4
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@550546f4
pool-1-thread-1号线程开始执行任务====================================!
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@513ac88c
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@513ac88c
pool-1-thread-1号线程开始执行任务====================================!
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@14384ccf
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@14384ccf
pool-1-thread-1号线程开始执行任务====================================!
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@5a2bd0f7
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@5a2bd0f7
pool-1-thread-1号线程开始执行任务====================================!
线程池关闭后执行 terminated();
Process finished with exit code 0