多线程
为什么使用多线程
# 1、随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。榨干cpu
# 2、降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
# 3、提高响应速度:任务到达时,无需等待线程创建即可立即执行。
# 4、提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
# 5、提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
主要解决什么问题,有哪些应用场景
# 1、频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
# 2、对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
# 3、系统无法合理管理内部的资源分布,会降低系统的稳定性。
多线程的基本知识
线程的创建
继承Thread
@Slf4j
public class MyThread extends Thread {
@Override
public void run() {
log.info("我是继承Thread创建的线程");
}
}
实现Runnable
@Slf4j
public class MyRunnable implements Runnable {
@Override
public void run() {
log.info("我是实现Runnable创建的线程");
}
}
实现Callable
@Slf4j
public class MyCallable implements Callable<String> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public String call() throws Exception {
log.info("我是实现Callable创建的线程");
return "MyCallable";
}
}
测试
@Slf4j
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
myThread.setName("myThread");
myThread.start();
MyRunnable myRunnable = new MyRunnable();
Thread threadRunnable = new Thread(myRunnable);
threadRunnable.setName("myRunnable");
threadRunnable.start();
MyCallable myCallable = new MyCallable();
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> submit = executor.submit(myCallable);
submit.get();
log.info("我是主线程");
}
}
结果
19:03:04.388 [myRunnable] INFO com.xd.archman.server.study.create.thread.MyRunnable - 我是实现Runnable创建的线程
19:03:04.388 [myThread] INFO com.xd.archman.server.study.create.thread.MyThread - 我是继承Thread创建的线程
19:03:04.388 [pool-1-thread-1] INFO com.xd.archman.server.study.create.thread.MyCallable - 我是实现Callable创建的线程
19:03:04.391 [main] INFO com.xd.archman.server.study.create.thread.TestThread - 我是主线程
线程池的使用
参数说明
public ThreadPoolExecutor(
int corePoolSize,# 核心线程数目 (最多保留的线程数)
int maximumPoolSize, # 最大线程数目
long keepAliveTime,# 生存时间 - 针对救急线程
TimeUnit unit,# 时间单位 - 针对救急线程
BlockingQueue<Runnable> workQueue,# 阻塞队列
RejectedExecutionHandler handler) # 拒绝策略
{
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), handler);
}
# threadFactory 线程工厂 - 可以为线程创建时起个好名字
阻塞队列
拒绝策略
参数流程图
# corePoolSize, maximumPoolSize workQueue
2 4 10
任务数
2 2
10 2 8
12 2 10
14 2 2 10
15 2 2 10 + 拒绝策略 1
线程池的类型
newFixedThreadPool
创建方式
# 该线程池是一种线程数量固定的线程池。在这个线程池中,所容纳的最大线程数就是就是设置的核心线程数。如果线程池中的线程处于空闲状态的话,并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就会处于等待状态,直到有线程空闲出来。
ExecutorService service = Executors.newFixedThreadPool(4);
# 源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
使用场景
# 适用于任务量已知,相对耗时的任务
newCachedThreadPool
创建方式
# 他是一个核心线程数为 0,最大线程数为 Integer.MAX_VALUE
# 当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60 秒,所以当线程处于闲置状态超过 60 秒的时候便会被回收。 这也就意味着若是整个线程池的线程都处于闲置状态超过60 秒以后,在newCachedThreadPool 线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。
ExecutorService executorService = Executors.newCachedThreadPool();
#源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
使用场景
# 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
newSingleThreadExecutor
创建方式
# 线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
ExecutorService executorService = Executors.newSingleThreadExecutor();
# 源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用场景
# 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
newScheduledThreadPool
创建方式
# 它的核心线程数是固定的,对于非核心线程几乎可以说是没有限制的,并且当非核心线程处于限制状态的时候就会立即被回收。创建一个可定时执行或周期执行任务的线程池
ExecutorService executorService = Executors.newScheduledThreadPool();
# 源码
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
# 使用方式
executorService.schedule(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "延迟三秒执行");
}
}, 3, TimeUnit.SECONDS);
executorService.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "延迟三秒后每隔2秒执行");
}
}, 3, 2, TimeUnit.SECONDS);
使用场景
# 延时 定时任务
Springboot中多线程的应用
@EnableAsync && @Async
# @EnableAsync 开启异步支持
# @Async
# 该注解可以标记一个异步执行的方法,也可以用来标注类,表示类中的所有方法都是异步执行的。
# 入参随意,但返回值只能是void或者Future.(ListenableFuture接口/CompletableFuture类)
# Future是代理返回的是实际异步返回的结果,用以追踪异步方法的返回值。当然也可以使用AsyncResult类(实现ListenableFuture接口)(Spring或者EJB都有)或者CompletableFuture类
# 加在类上表示整个类都使用,加在方法上会覆盖类上的设置,只有当前方法生效
# 默认使用的是:SimpleAsyncTaskExecutor 每次都会创建一条线程处理任务
# value字段用以限定执行方法的执行器名称(自定义):Executor或者TaskExecutor
# 调用 同类中调用异步方法不生效
自定义线程池
定义规则:
80%:交给核心线程数处理
每个线程执行时间200ms QPS:100 要求 :1s core = 单个线程执行时间 * QPS = 0.2 * 100 = 20
20%:交给最大线程数+队列处理
对列长度: queue = core/执行时间 = 20/0.2 = 100
最大线程数目 : maxCore = (最大QPS - 队列长度) / 每条线程执行时间 = (1000 - 100)* 0.2 = 180
/**
* 抛出异常策略
*/
@Bean("abortPolicy")
public Executor abortPolicy() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:线程池创建时候初始化的线程数
executor.setCorePoolSize(2);
// 最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(4);
// 缓冲队列:用来缓冲执行任务的队列
executor.setQueueCapacity(10);
// 允许线程的空闲时间10秒:当超过了核心线程之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(10);
// 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("abort-test-");
// 调用者线程中直接执行该被拒绝任务的run方法
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
return executor;
}
多线程的使用
代码演示
# 演示参数流程
com.xd.archman.server.Acreate.strategy.abortPolicy.AbortPolicyService#one
# 演示默认线程池
重点源码解读
# 扫描 @EnableAsync
org.springframework.scheduling.annotation.AbstractAsyncConfiguration#setImportMetadata
# 线程池的定义
org.springframework.scheduling.annotation.AsyncConfigurer
org.springframework.scheduling.annotation.AbstractAsyncConfiguration#setConfigurers
# 异步方法未捕捉的异常处理
org.springframework.scheduling.annotation.AsyncConfigurer
org.springframework.scheduling.annotation.AbstractAsyncConfiguration#setConfigurers
# 扫描 @Async
org.springframework.scheduling.annotation.AsyncAnnotationAdvisor#AsyncAnnotationAdvisor(java.util.function.Supplier<java.util.concurrent.Executor>, java.util.function.Supplier<org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler>)
org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor#getExecutorQualifier
# 返回值的判断
org.springframework.aop.interceptor.AsyncExecutionAspectSupport#doSubmit
# 切面
org.springframework.scheduling.annotation.AsyncAnnotationAdvisor#buildPointcut
# 总结
AOP--------IOC-------PROXY
找 -> 切 -> 放 -> 拿 -> 用
平时遇到的一些问题
细粒度的划分
一切不基于实际业务的设计,都是耍流氓。
# 以dr首页为例
五个接口 串行调用 每个接口算 200ms 总共1s 最后响应到前台进行渲染 大概 1.2s
# 改为并行如何划分呢?
可以根据实际业务出发。
可以明显看出:一共五个接口,每个接口对应一个服务。调用时间大致相同。并行后可以减少到200ms完成5个接口的调用。
异常如何处理
异步任务
# 线程配置类中实现 public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler()
捕获异常后,打印或者写入日志,然后通知到平台
同步任务
# 直接捕获处理
线程上下文处理
# ThreadLocal : 每个线程独立拥有自己的线程局部变量,因此是线程安全的
# InheritableThreadLocal : 可以在父子线程中传递数据
getMap和 createMap 让本地变量保存到了具体线程的inheritableThreadLocals 变量里面, 那么线程在通过InheritableThreadLocal 类实例的set 或者get 方法设置变量时, 就会创建当前线程的inheritableThreadLocals 变量 。 当父线程创建子线程时, 构造函数会把父线程中inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 InheritableThreadLocal
# TransmittableThreadLocal : 在使用线程池等会缓存线程的组件情况下传递数据
在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
内存泄露
当异步线程创建了上下文对象和线程的成员变量后。关系如下:
Entry的key为ThreadLocal,value为成员变量,当线程结束后,弱引用会被gc清理,而ThreadLocal的成员变量就不会
解决方式:及时清理
CompletableFuture
Java8 CompletableFuture 用法全解孙大圣666的博客-CSDN博客completablefuture
并行
有返回值
/**
* 创建异步执行任务,有返回值
*/
@Test
public void test1() throws ExecutionException, InterruptedException {
// 任务一
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
MySleepUtils.sleep(1);
return "任务一";
}, executorService);
// 任务二
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
MySleepUtils.sleep(1);
return "任务二";
}, executorService);
//等待子任务执行完成
log.info("结果:{} || {}", task1.get(), task2.get());
}
// 结果
09:59:45.882 [main] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 结果:任务一 || 任务二
无返回值
/**
* 创建异步执行任务,无返回值
*/
@Test
void test2() {
// 任务一
CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
MySleepUtils.sleep(1);
log.info("任务一");
}, executorService);
// 任务二
CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> {
MySleepUtils.sleep(1);
log.info("任务二");
}, executorService);
task1.join();
task2.join();
}
// 结果
10:00:36.849 [pool-1-thread-2] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 任务二
10:00:36.849 [pool-1-thread-1] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 任务一
串行
/**
* 创建异步执行任务,有返回值
*/
@Test
void test3() throws ExecutionException, InterruptedException {
// 任务一
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
MySleepUtils.sleep(1);
log.info("任务一");
return "任务一";
}, executorService);
// 任务二
CompletableFuture<String> task2 = task1.thenApplyAsync((result) -> {
MySleepUtils.sleep(1);
log.info("任务二获取到任务一的结果:{}", result);
return "任务二";
});
//等待子任务执行完成
log.info("结果:{} || {}", task1.get(), task2.get());
}
// 结果
10:11:22.158 [pool-1-thread-1] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 任务一
10:11:23.162 [ForkJoinPool.commonPool-worker-25] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 任务二获取到任务一的结果:任务一
10:11:23.163 [main] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 结果:任务一 || 任务二
有并有串
/**
* 创建 两并一串
*/
@Test
void test4() throws ExecutionException, InterruptedException {
// 任务一
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
MySleepUtils.sleep(1);
log.info("任务一");
return "任务一";
}, executorService);
// 任务二
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
MySleepUtils.sleep(1);
log.info("任务二");
return "任务二";
}, executorService);
// 任务三
CompletableFuture<String> task3 = task1.thenCombine(task2, (r1, r2) -> {
MySleepUtils.sleep(1);
log.info("任务三获取到的任务一数据:{},任务二数据:{}", r1, r2);
return "任务三";
});
log.info("结果:{}-{}-{}", task1.get(), task2.get(), task3.get());
}
// 结果
10:24:47.492 [pool-1-thread-1] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 任务一
10:24:47.492 [pool-1-thread-2] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 任务二
10:24:48.495 [pool-1-thread-2] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 任务三获取到的任务一数据:任务一,任务二数据:任务二
10:24:48.496 [main] INFO com.xd.archman.server.Boot.study.TestCompletableFuture - 结果:任务一-任务二-任务三
参考文献
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com)
(1条消息) @Async源码解析,从注释开始讲解Henry-tech的博客-CSDN博客async源码
@Async 深度源码解析_weixin_46202666的博客-CSDN博客
Java中的异常处理:何时抛出异常,何时捕获异常?-百度网盘下载-Java自学者论坛 - Powered by Discuz! (javazxz.com)
🍃【Spring技术实战】@Async机制的使用技巧以及异步注解源码解析 - 掘金 (juejin.cn)
【并发编程020】具体说说InheritableThreadLocal 是如何让子线程可以访问在父线程 中设置的本地变量的?_檀越剑指大厂的博客-CSDN博客
ThreadLocal为什么会导致内存泄漏?客官莫回头的博客-CSDN博客threadlocal内存泄漏原因