多线程的学习和使用

152 阅读11分钟

多线程

为什么使用多线程

# 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 线程工厂 - 可以为线程创建时起个好名字

image-20220523112859802

阻塞队列

image-20220517194313796

拒绝策略

image-20220517194400457

参数流程图
#       corePoolSize, maximumPoolSize workQueue
            2           4               10
任务数
2           2
10          2                           8
12          2                           10
14          2           2               10
15          2           2               10      +  拒绝策略  1

image-20220523112859802

线程池的类型

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
找 -> 切 -> 放 -> 拿 -> 用

平时遇到的一些问题

细粒度的划分

一切不基于实际业务的设计,都是耍流氓。

image-20220522162129524

# 以dr首页为例
五个接口 串行调用 每个接口算 200ms 总共1s 最后响应到前台进行渲染 大概 1.2s
​
# 改为并行如何划分呢?
可以根据实际业务出发。 
可以明显看出:一共五个接口,每个接口对应一个服务。调用时间大致相同。并行后可以减少到200ms完成5个接口的调用。

异常如何处理

异步任务

# 线程配置类中实现        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler()
捕获异常后,打印或者写入日志,然后通知到平台

同步任务

# 直接捕获处理

线程上下文处理

# ThreadLocal : 每个线程独立拥有自己的线程局部变量,因此是线程安全的# InheritableThreadLocal :  可以在父子线程中传递数据
​
getMap和 createMap 让本地变量保存到了具体线程的inheritableThreadLocals 变量里面, 那么线程在通过InheritableThreadLocal 类实例的set 或者get 方法设置变量时, 就会创建当前线程的inheritableThreadLocals 变量 。 当父线程创建子线程时, 构造函数会把父线程中inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 InheritableThreadLocal
​
# TransmittableThreadLocal : 在使用线程池等会缓存线程的组件情况下传递数据
在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

alibaba/transmittable-thread-local: 📌 TransmittableThreadLocal (TTL), the missing Java™ std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components. (github.com)

内存泄露

当异步线程创建了上下文对象和线程的成员变量后。关系如下:
Entry的key为ThreadLocal,value为成员变量,当线程结束后,弱引用会被gc清理,而ThreadLocal的成员变量就不会
解决方式:及时清理

image-20220522211857016

CompletableFuture

Java8 CompletableFuture 用法全解孙大圣666的博客-CSDN博客completablefuture

并行

有返回值

image-20220523093920693

    /**
     * 创建异步执行任务,有返回值
     */
    @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 - 任务一

串行

image-20220523100517960

/**
     * 创建异步执行任务,有返回值
     */
    @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 - 结果:任务一 || 任务二
​

有并有串

image-20220523101223771

    /**
     * 创建 两并一串
     */
    @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内存泄漏原因

Spring的@EnableAsync与@Async使用详解若鱼1919的博客-CSDN博客@enableasync

@Async Spring异步任务的深入学习与使用刘Java的博客-CSDN博客spring异步任务