首页 > 编程语言 >多线程篇(并发编程 - Java线程实现方式)(持续更新迭代)

多线程篇(并发编程 - Java线程实现方式)(持续更新迭代)

时间:2024-09-01 09:25:11浏览次数:11  
标签:Java Thread System 任务 线程 new 多线程 public

目录

一、继承Thread类

1. 简介

2. 实现

2.1. 原始方式

2.2. Lambda 表达式

二、实现Runnable接口

1. 简介

2. 实现

2.1. 原始方式

2.2. Lambda 表达式

三、使用FutureTask

1. 简介

2. 实现

2.1. 原始方式

2.2. Lambda 表达式

四、使用线程池

1. ThreadPoolExecutor

newCachedThreadPool

newFixedThreadPool

newScheduledThreadPool

newSingleThreadExecutor

2. ThreadPoolTaskExecutor

2.1. 创建线程池

2.2. 编写多线程方法

2.3. 编写测试调用多线程方法

3. ThreadPoolTaskExecutor、ThreadPoolExecutor区别

3.1. ThreadPoolExecutor

线程池接口:ExecutorService

线程池的体系结构

工具类: Executors

3.2. ThreadPoolTaskExecutor

拒绝策略

处理流程

3.3. 知识小结

4. SpringBoot中@Async多线程注解使用

4.1. 创建线程池

4.2. 编写多线程方法

4.3. 编写测试调用多线程方法

4.4. 注意

4.5. 使用案例

4.6. 知识小结

5. 手写线程池的几种方式

5.1. 阿里巴巴为什么不建议直接使用Async注解?

5.2. 应用场景

5.3. Spring 已经实现的线程池

5.4. 异步的方法

Spring中启用@Async

无返回值调用

有返回值Future调用

5.5. @Async应用自定义线程池

五、扩展:CompletionStage(待更新)

六、扩展:CompletableFuture(待更新)


一、继承Thread类

1. 简介

自定义线程 ThreadDemo,ThreadDemo类继承了Thread类,并重写了 run() 方法。

在 main 函数里面创建了一个 MyThread 的实例,然后调用该实例的 start 方法启动了线程。

当创建完 thread 对象后该线程并没有被启动执行,直到调用了start方法后才真正启动了线程,也就是说在线程未

调用 start()方法时,前面所走的程序还是属于单线程程序,那么 start 就意味着开启多线程!

其实调用 start 方法后线程并没有马上执行而是处于就绪状态(可运行状态),这个就绪状态

(可运行状态)是指该线程已经获取了除 CPU 资源外的其他资源,等待获取 CPU 资源后才会

真正处于运行状态。

一旦 run 方法(封装的任务代码)执行完毕,该线程就处于终止状态。

使用继承方式的好处是,在 run() 方法内获取当前线程直接使用 this 就可以了,无须使用

Thread.currentThread()方法;

不好的地方是 Java 不支持多继承,如果继承了Thread 类那么就不能再继承其他类。

另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而 Runable 则没

有这个限制。

细节问题:如果没有给线程命名,那么线程的默认名称就是Thread-x,x是序号,从0开始!

2. 实现

2.1. 原始方式

/**
 * 创建线程方式一:继承Thread
 */
public class MyThreadDemo {
    public static void main(String[] args) {
        // 3. new一个新线程对象
        Thread t = new MyThread();
        // 4. 调用start方法启动线程(执行的还是run方法)
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 主线程输出:" + i);
        }

    }
}

/**
 * 1. 定义一个线程类继承Thread类
 */
class MyThread extends Thread{
    /**
     * 2. 重写run方法,里面是定义线程以后要干啥
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 子线程输出:" + i);
        }
    }
}

2.2. Lambda 表达式

public class MyThreadLambdaDemo {
    public static void main(String[] args) {

        Thread thread = new Thread( () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " 子线程输出:" + i);
            }
        },"yjxz");

        // new Thread().start()在没有执行到start那一步之前,走的是单线程程序
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 主线程输出:" + i);
        }
        // thread.start();// 在线程未调用start()方法时,前面所走的程序还是属于单线程程序,可在次验证!
    }

}

二、实现Runnable接口

1. 简介

两个线程共用一个 task 代码逻辑,如果需要,可以给 RunableTask 添加参数进行任务区分。

另外,RunableTask 可以继承其他类,任务交给线程处理。

2. 实现

2.1. 原始方式

/**
 * 创建线程方式二:实现Runnable接口
 */
public class MyRunnableDemo {
    public static void main(String[] args) {
        // 3. 创建一个任务对象
        Runnable target = new MyRunnable();
        // 4. 把任务对象交给Thread处理
        Thread t = new Thread(target);
        // Thread t = new Thread(target, "yjxz1");
        // 5. 启动线程
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 主线程输出:" + i);
        }
    }
}

/**
   1.定义一个线程任务类 实现Runnable接口
 */
class MyRunnable  implements Runnable {
    /**
       2. 重写run方法,定义线程的执行任务的
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 子线程输出:" + i);
        }
    }
}

2.2. Lambda 表达式

/**
   匿名内部类 + Lambda表达式两种方式是心啊
 */
public class MyRunnableLamdaDemo {
    public static void main(String[] args) {
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " 子线程1输出:" + i);
                }
            }
        };
        Thread t = new Thread(target);
        t.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " 子线程2输出:" + i);
                }
            }
        }).start();

        new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " 子线程3输出:" + i);
            }
        }).start();

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " 主线程输出:" + i);
        }
    }
}

三、使用FutureTask

1. 简介

但是上面的两种实现方式都有一个缺点,就是任务没有返回值,这个时候,我们可以借助

FutureTask 的方式。

思路整理:任务对象交给未来任务对象,未来任务对象交给线程对象,线程启动,调用任务,未

来任务对象获取结果,获取过程中需等待任务对象计算完毕!

我们知道 CallerTask 类实现了 Callable 接口的 call() 方法。

在 main 函数内首先创建了一个 FutrueTask 对象(构造函数为 CallerTask 的实例),然后使用创建

的 FutrueTask对象作为任务创建了一个线程并且启动它,最后通过 futureTask.get() 等待任务执

行完毕并返回结果。

2. 实现

2.1. 原始方式

/**
   创建方式三:实现Callable接口,结合FutureTask完成
 */
public class MyCallableDemo {
    public static void main(String[] args) {
        // 3. 创建Callable任务对象
        Callable<String> call = new MyCallable(100);
        // 4. 把Callable任务对象 交给 FutureTask 对象
        //  FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
        //  FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
        FutureTask<String> f1 = new FutureTask<>(call);
        // 5、FutureTask 对象交给线程处理
        Thread t1 = new Thread(f1);
        // 6、启动线程
        t1.start();


        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();

        try {
            // 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
            String rs1 = f1.get();
            System.out.println("第一个结果:" + rs1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            // 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
            String rs2 = f2.get();
            System.out.println("第二个结果:" + rs2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/**
 1. 定义一个任务类 实现Callable接口,并声明线程任务执行完毕后的结果的数据类型
 */
class MyCallable implements Callable<String>{
    private int num;
    public MyCallable(int n) {
        this.num = num;
    }

    /**
       2. 重写call方法(任务方法)
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= num ; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + " 子线程执行的结果是:" + sum;
    }
}

2.2. Lambda 表达式

/**
 * ClassName:MyCallableLambdaDemo
 * Package:PACKAGE_NAME
 * Description:描述
 *
 * @Date:2022/7/15 15:19
 * @Author:NieZheng
 * @Version:1.0
 */
public class MyCallableLambdaDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(Thread.currentThread().getName() + " 开始运行...");
        FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int total = 0;
                for (int i = 0; i < 100; ++ i) {
                    if (Thread.currentThread().isInterrupted()){
                        System.out.println("任务取消了");
                        return total;
                    }
                    total += i;
                    System.out.println(Thread.currentThread().getName() + " total = " + total);
                }
                return total;
            }
        });

        Thread th = new Thread(task);
        th.start();

        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

//        task.cancel(true); // 取消任务

        int num = task.get(); // 获取线程运行完成后的值
        System.out.println(num);
    }
}

四、使用线程池

1. ThreadPoolExecutor

这个类是 JDK 中的线程池类,继承自 Executor

newCachedThreadPool

创建一个线程池,如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,那么它可以

创建新的线程。

public class CacheThreadPoolTest {
    private static int counter = 0;
    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();
        //提交100个任务
        for(int i = 0; i < 1000; i++) {
            //submit()会异常处理 底层-> excute(new Runnable(){})
            threadPool.execute(new Add());
//            threadPool.submit(new Add());
        }
        threadPool.shutdown();
        //打印最多使用线程数
        System.out.println("核心线程数: " +threadPool.getCorePoolSize());
        System.out.println("Largest线程数: " +threadPool.getLargestPoolSize());
        System.out.println("同时执行的Maximum线程数: " +threadPool.getMaximumPoolSize());
        System.out.println(counter);
    }

    static class Add implements Runnable {
        @Override
        public void run() {
            counter++;
        }
    }
}

newFixedThreadPool

这种方式可以指定线程池中的线程数。

举个栗子,如果一间澡堂子最大只能容纳20个人同时洗澡,

那么后面来的人只能在外面排队等待。

public class FixedThreadPoolTest {
    private static int counter = 0;
    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
        //提交1000个任务
        for(int i = 0; i < 1000; i++) {
            //submit()会异常处理 底层-> excute(new Runnable(){})
            threadPool.submit(new Add());
        }
        threadPool.shutdown();
        //打印最多使用线程数
        System.out.println("核心线程数: " +threadPool.getCorePoolSize());
        System.out.println("Largest线程数: " +threadPool.getLargestPoolSize());
        System.out.println("队列: " +threadPool.getQueue());
        System.out.println("同时执行的Maximum线程数: " +threadPool.getMaximumPoolSize());
        System.out.println(counter);
    }

    static class Add implements Runnable {
        @Override
        public void run() {
            counter++;
        }
    }
}

newScheduledThreadPool

该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周

期性的时间让任务重复执行。

定时任务默认是单线程执行,在实际应用场景中可以通过配置 定时任务线程池,实现定时任务

并发执行

public class ScheduledThreadPoolTest {
    private static int counter = 0;
    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newScheduledThreadPool(10);
        //提交1000个任务
        for(int i = 0; i < 1000; i++) {
            //submit()会异常处理 底层-> excute(new Runnable(){})
            threadPool.submit(new Add());
        }
        threadPool.shutdown();
        //打印最多使用线程数
        System.out.println("核心线程数: " +threadPool.getCorePoolSize());
        System.out.println("Largest线程数: " +threadPool.getLargestPoolSize());
        System.out.println("同时执行的Maximum线程数: " +threadPool.getMaximumPoolSize());
        System.out.println(counter);
    }

    static class Add implements Runnable {
        @Override
        public void run() {
            counter++;
        }
    }
}

newSingleThreadExecutor

这是一个单线程池,至始至终都由一个线程来执行。

public class SingleThreadPoolTest {
    private static int counter = 0;
    public static void main(String[] args) throws Exception {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //提交1000个任务
        for(int i = 0; i < 1000; i++) {
            //submit()会异常处理 底层-> excute(new Runnable(){})
            threadPool.submit(new Add());
        }
        threadPool.shutdown();
        //打印最多使用线程数
        System.out.println(counter);
    }

    static class Add implements Runnable {
        @Override
        public void run() {
            counter++;
        }
    }
}

2. ThreadPoolTaskExecutor

这个类则是spring包下的,是sring为我们提供的线程池类

通过配置类的方式配置线程池,然后注入。(或者spring的配置文件)

2.1. 创建线程池

@EnableAsync
@Configuration
public class ExecturConfig {
    @Bean("taskExector")
    public Executor taskExector() {
 
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int i = Runtime.getRuntime().availableProcessors();//获取到服务器的cpu内核
        executor.setCorePoolSize(10);//核心池大小
        //executor.setMaxPoolSize(100);//最大线程数
        //executor.setQueueCapacity(1000);//队列程度
      //executor.setKeepAliveSeconds(1000);//线程空闲时间
      //executor.setThreadNamePrefix("tsak-asyn");//线程前缀名称
        //executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//配置拒绝策略
        return executor;

然后通过自动注入的方式注入线程池

2.2. 编写多线程方法

@Resource(name=“taskExecutor”)
ThreadPoolTaskExecutor taskExecutor;
// 或者可以直接@Autowried
@AutoWired
ThreadPoolTaskExecutor taskExecutor
@Component
public class ServerStarter{
   @Async("taskExector")
   public void startServer(){
    //业务逻辑
    }
}

2.3. 编写测试调用多线程方法

//测试
public static void main(String[] args) {
    new ServerStarter().startServer();
}

构造函数中调用,目的就是在类的初始化的时候进行调用。

//实际开发
@Component
public class Server {

    public Server(ServerStarter starter) {
        starter.startServer();
    }
}

3. ThreadPoolTaskExecutor、ThreadPoolExecutor区别

之前工作中发现有同事在使用线程池的时候经常搞混淆ThreadPoolTaskExecutor和

ThreadPoolExecutor,座椅在这里想写一片博客来讲讲这两个线程池的区别以及使用

3.1. ThreadPoolExecutor

这个类是JDK中的线程池类,继承自Executor, Executor 顾名思义是专门用来处理多线程相关

的一个接口,所有线程池相关的类都实现了这个接口,里面有一个execute()方法,用来执行线

程,线程池主要提供一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁的额

外开销,提高了响应的速度。相关的继承实现类图如下。

线程池接口:ExecutorService

线程池接口:ExecutorService为线程池接口,提供了线程池生命周期方法,继承自Executor接

口,ThreadPoolExecutor为线程池实现类,提供了线程池的维护操作等相关方法,继承自

AbstractExecutorService,AbstractExecutorService 实现了 ExecutorService 接口。

线程池的体系结构

java.util.concurrent.Executor 负责线程的使用和调度的根接口

|--ExecutorService 子接口: 线程池的主要接口

|--ThreadPoolExecutor 线程池的实现类

|--ScheduledExceutorService 子接口: 负责线程的调度

|--ScheduledThreadPoolExecutor : 继承ThreadPoolExecutor,实现了

ScheduledExecutorService

工具类: Executors

Executors为线程迟工具类,相当于一个工厂类,用来创建合适的线程池,返回ExecutorService

类型的线程池。

有人如下方法。

  • ExecutorService newFixedThreadPool() : 创建固定大小的线程池
  • ExecutorService newCachedThreadPool():缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
  • ExecutorService newSingleThreadExecutor() : 创建单个线程池。 线程池中只有一个线程

ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定

时的执行任务

其中AbstractExecutorService是他的抽象父类,继承自ExecutorService,ExecutorService 接口扩

展Executor接口,增加了生命周期方法

实际应用中我一般都比较喜欢使用Exectuors工厂类来创建线程池,里面有五个方法,分别创建

不同的线程池,如上,创建一个制定大小的线程池,Exectuors工厂实际上就是调用的

ExectuorPoolService的构造方法,传入默认参数。

public class Executors {
 
    /**
     * Creates a thread pool that reuses a fixed number of threads
     * operating off a shared unbounded queue.  At any point, at most
     * {@code nThreads} threads will be active processing tasks.
     * If additional tasks are submitted when all threads are active,
     * they will wait in the queue until a thread is available.
     * If any thread terminates due to a failure during execution
     * prior to shutdown, a new one will take its place if needed to
     * execute subsequent tasks.  The threads in the pool will exist
     * until it is explicitly {@link ExecutorService#shutdown shutdown}.
     *
     * @param nThreads the number of threads in the pool
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code nThreads <= 0}
     */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
 
    /**
     * Creates a thread pool that maintains enough threads to support
     * the given parallelism level, and may use multiple queues to
     * reduce contention. The parallelism level corresponds to the
     * maximum number of threads actively engaged in, or available to
     * engage in, task processing. The actual number of threads may
     * grow and shrink dynamically. A work-stealing pool makes no
     * guarantees about the order in which submitted tasks are
     * executed.
     *
     * @param parallelism the targeted parallelism level
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code parallelism <= 0}
     * @since 1.8
     */
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

当然,我们也可以直接new ThreadPoolExecutor的构造方法来创建线程池,传入需要的参数。

3.2. ThreadPoolTaskExecutor

这个类则是spring包下的,是sring为我们提供的线程池类,这里重点讲解这个类的用法,可以使

用基于xml配置的方式创建

    <!-- spring线程池 -->
    <bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <!-- 核心线程数  -->
        <property name="corePoolSize" value="10"/>
        <!-- 最大线程数 -->
        <property name="maxPoolSize" value="200"/>
        <!-- 队列最大长度 >=mainExecutor.maxSize -->
        <property name="queueCapacity" value="10"/>
        <!-- 线程池维护线程所允许的空闲时间 -->
        <property name="keepAliveSeconds" value="20"/>
        <!-- 线程池对拒绝任务(无线程可用)的处理策略 -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
        </property>

    </bean>

然后通过自动注入的方式注入线程池

@Resource(name="taskExecutor")
ThreadPoolTaskExecutor taskExecutor;
// 或者可以直接@Autowried
@AutoWired
ThreadPoolTaskExecutor taskExecutor

或者是通过配置类的方式配置线程池,然后注入。

@Configuration
public class ExecturConfig {
    @Bean("taskExector")
    public Executor taskExector() {
 
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int i = Runtime.getRuntime().availableProcessors();//获取到服务器的cpu内核
        executor.setCorePoolSize(5);//核心池大小
        executor.setMaxPoolSize(100);//最大线程数
        executor.setQueueCapacity(1000);//队列程度
        executor.setKeepAliveSeconds(1000);//线程空闲时间
        executor.setThreadNamePrefix("tsak-asyn");//线程前缀名称
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//配置拒绝策略
        return executor;
    }
}

上面注解中已经注释了参数的详解,这里重点讲解一下 spring 线程池的拒绝策略和处理流程。

上面注解中已经注释了参数的详解,这里重点讲解一下 spring 线程池的拒绝策略和处理流程。

拒绝策略

rejectedExectutionHandler参数字段用于配置绝策略,常用拒绝策略如下:

  1. AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException
  2. CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
  3. DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
  4. DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
处理流程
  1. 查看核心线程池是否已满,不满就创建一条线程执行任务,否则执行第二步。
  2. 查看任务队列是否已满,不满就将任务存储在任务队列中,否则执行第三步。
  3. 查看线程池是否已满,即就是是否达到最大线程池数,不满就创建一条线程执行任务,否则就按照策略处理无法执行的任务。

流程图如下:

3.3. 知识小结

这里主要讲了一下 JDK 线程池和 spring 线程池这两个线程池,

具体实际业务则和平时使用一样。

接下来将讲一下如何使用 spring 的异步多线程调用注解 @Async 使用。

4. SpringBoot中@Async多线程注解使用

在没有@Async注解之前我们需要写一个多线程低吗的话需要使用到JDK原生的多线程方法,代

码十分冗余,当有了Spring的@Async注解后就十分方便了,本次就详细介绍一下@Async注解

4.1. 创建线程池

当在一个方法上标注了@Async注解之后,在被调用的时候主线程会主动使用多线程来调用此方

法,但是当我们需要线程池来对多线程进行管理的时候就需要使用到配置类线程池的Bean,

如下:

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean("getTaskExector")
    public Executor taskExecutor() {
        //通过Runtime方法来获取当前服务器cpu内核,根据cpu内核来创建核心线程数和最大线程数
        int threadCount = Runtime.getRuntime().availableProcessors();
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(threadCount);
        executor.setMaxPoolSize(threadCount);
        executor.setQueueCapacity(200);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("taskExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
    
}

这样就会申明一个线程池Bean,

4.2. 编写多线程方法

在使用多线程方法上标注@Async时表明调用的线程池,如下

import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import java.util.concurrent.Future;

@Component
public class Async {
    
   @Async("getTaskExector")
   public void doTaskThree() throws Exception {
        System.out.println("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(10000);
        long end = System.currentTimeMillis();
        System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
    }
}

4.3. 编写测试调用多线程方法

public static void main(String[] args) {

        Async async = new Async();
        for (int i=0;i<=20;i++){
            async.doTaskThree();
        }
             

}

这样写出来的多线程代码是不是很简洁呢

4.4. 注意

标注@Async注解的方法和调用的方法一定不能在同一个类下,这样的话注解会失效,具体原因

不多说接下来将会写一下项目中用到的实际案例Demo,SpringBoot异步多线程调用注解

@Async使用和CountDownLatch配合使用案例

4.5. 使用案例

关于多线成调用可能大家用的比较多的是JDK的多线程,springboot1.5+,项目框架中集成了异步

多线程操作配置,在这里和大家分享一下springboot的异步多线程注解使用,先一步一步来以代码

的形式讲解大家可能会遇到的问题。

一:创建方法,然后在方法上添加 @Async 注解,然后还需要在 @SpringBootApplication 启动

类或者@configure 注解类上 添加注解@EnableAsync启动多线程注解,@Async就会对标注的方

法开启异步多线程调用,注意,这个方法的类一定要交给spring容器来管理

@Component
public class Test {
    //注意这个多线程方法的类一定要加@Component注解,拿给spring容器管理
    @Async
    public void doTaskThree(int i) {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("第00" + i + "完成任务,耗时:" + (end - start) + "毫秒,线成名为::" + Thread.currentThread().getName());
 
    }
}

然后 其他方法就可以直接调用此方法了,spring会开启多线程异步调用。

注意:关于注解失效需要注意以下几点

  1. 注解的方法必须是 public 方法
  2. 方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。
  3. 异步方法使用注解 @Async 的返回值只能为void或者Future
    @Autowired
    private Test test;
    @Override
    public List<Integer> findByMonth() {
        List<Integer> list = new ArrayList<>();
        for (int i =1;i<=12;i++){
            int count= baseMapper.findByMonth(i);
            list.add(count);
        }
        System.out.println("开始执行多线程任务1111111111:::"+System.currentTimeMillis());
        for (int i =0;i<=5;i++){
            test.doTaskThree(i);
        }
        System.out.println("主线程继续执行222222222222222:::::"+Thread.currentThread().getName());
        return list;
    }

然后用postman调用发现了一个问题如下,主线程根本没有等待子线程运行完就直接往下执行并

且返回,

开始执行多线程任务1111111111:::1576198126421
主线程继续执行222222222222222:::::http-nio-8300-exec-1
第001完成任务,耗时:10000毫秒,线成名为::tsak-asyn2
第002完成任务,耗时:10000毫秒,线成名为::tsak-asyn3
第005完成任务,耗时:10000毫秒,线成名为::tsak-asyn6
第000完成任务,耗时:10000毫秒,线成名为::tsak-asyn1
第004完成任务,耗时:10000毫秒,线成名为::tsak-asyn5
第003完成任务,耗时:10000毫秒,线成名为::tsak-asyn4

这种情况肯定不是我们想要的,那么我们怎么才能让主线程等待子线程呢?

不知道大家有没有了解过多线程里面的CountDownLatch、Semaphone,CyclicBarrier,们怎么

在这里我们就可以用CountDownLatch或者CyclicBarrier来解决我们现在遇到的问题 关于他们的

了解后续我会写一点自己的,如果大家有需要可以到网上去找找相关博客学习

CountDownLatch、Semaphone学习

    @Autowired
    private Test test;
    @Override
    public List<Integer> findByMonth() {
        List<Integer> list = new ArrayList<>();
        for (int i =1;i<=12;i++){
            int count= baseMapper.findByMonth(i);
            list.add(count);
        }
        CountDownLatch countDownLatch = new CountDownLatch(6);
        System.out.println("开始执行多线程任务1111111111:::"+System.currentTimeMillis());
        for (int i =0;i<=5;i++){
            test.doTaskThree(countDownLatch,i);
 
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程继续执行222222222222222:::::"+Thread.currentThread().getName());
        return list;

@Component
public class Test {
    //注意这个多线程方法的类一定要加@Component注解,拿给spring容器管理
    @Async
    public void doTaskThree(CountDownLatch countDownLatch,int i) {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("第00" + i + "完成任务,耗时:" + (end - start) + "毫秒,线成名为::" + Thread.currentThread().getName());
        countDownLatch.countDown();
    }
 
}

修改之后我们再来看看结果呢,这下我们看到结果按照我们想要的来执行了

开始执行多线程任务1111111111:::1576198763725
第005完成任务,耗时:10000毫秒,线成名为::SimpleAsyncTaskExecutor-6
第004完成任务,耗时:10000毫秒,线成名为::SimpleAsyncTaskExecutor-5
第000完成任务,耗时:10000毫秒,线成名为::SimpleAsyncTaskExecutor-1
第003完成任务,耗时:10000毫秒,线成名为::SimpleAsyncTaskExecutor-4
第002完成任务,耗时:10000毫秒,线成名为::SimpleAsyncTaskExecutor-3
第001完成任务,耗时:10000毫秒,线成名为::SimpleAsyncTaskExecutor-2
主线程继续执行222222222222222:::::http-nio-8300-exec-1

但是我们连续调用还是发现了一个问题,那就是每次都是重新开启线程,大家知道多线程的创建

和销毁是非常消耗cpu资源的,所以怎么解决呢?那就是使用spring的线程池,然后给@Async属

性赋值@Async("getTaskExector"),表示使用此线程池。

    @Bean("getTaskExector")
    public Executor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int threadCount = Runtime.getRuntime().availableProcessors();
        executor.setCorePoolSize(threadCount);//核心池大小
        executor.setMaxPoolSize(threadCount);//最大线程数
        executor.setQueueCapacity(1000);//队列程度
        executor.setKeepAliveSeconds(1000);//线程空闲时间
        executor.setThreadNamePrefix("tsak-asyn");//线程前缀名称
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//配置拒绝策略
        return executor;
    }

经过多次调用发现使用了线程池里面的线成重复使用了

开始执行多线程任务1111111111:::1576200103134
第002完成任务,耗时:10000毫秒,线成名为::tsak-asyn3
第001完成任务,耗时:10000毫秒,线成名为::tsak-asyn2
第003完成任务,耗时:10000毫秒,线成名为::tsak-asyn4
第005完成任务,耗时:10000毫秒,线成名为::tsak-asyn6
第004完成任务,耗时:10000毫秒,线成名为::tsak-asyn5
第000完成任务,耗时:10000毫秒,线成名为::tsak-asyn1
主线程继续执行222222222222222:::::http-nio-8300-exec-3
 
开始执行多线程任务1111111111:::1576200126619
第003完成任务,耗时:10000毫秒,线成名为::tsak-asyn2
第004完成任务,耗时:10000毫秒,线成名为::tsak-asyn4
第001完成任务,耗时:10000毫秒,线成名为::tsak-asyn8
第000完成任务,耗时:10000毫秒,线成名为::tsak-asyn7
第005完成任务,耗时:10000毫秒,线成名为::tsak-asyn6
第002完成任务,耗时:10000毫秒,线成名为::tsak-asyn3
主线程继续执行222222222222222:::::http-nio-8300-exec-2
 
开始执行多线程任务1111111111:::1576200167044
第005完成任务,耗时:10001毫秒,线成名为::tsak-asyn7
第003完成任务,耗时:10001毫秒,线成名为::tsak-asyn4
第004完成任务,耗时:10001毫秒,线成名为::tsak-asyn8
第000完成任务,耗时:10001毫秒,线成名为::tsak-asyn5
第002完成任务,耗时:10001毫秒,线成名为::tsak-asyn2
第001完成任务,耗时:10001毫秒,线成名为::tsak-asyn1
主线程继续执行222222222222222:::::http-nio-8300-exec-1

还有一种就是使用多线程中带返回的 Future 结果来进行主线程的控制,大概如下,可供参考。

    @Async("getTaskExector")
    public Future<String> doTaskThree(int i) {
        long start = System.currentTimeMillis();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("第00" + i + "完成任务,耗时:" + (end - start) + "毫秒,线成名为::" + Thread.currentThread().getName());
        return new AsyncResult("SUCUESS");
    }
 

    System.out.println("开始执行多线程任务1111111111:::"+System.currentTimeMillis());
        List<Future<String>> list1 = new ArrayList<>();
        for (int i =0;i<=5;i++){
            Future<String> stringFuture = test.doTaskThree(i);
            list1.add(stringFuture);
        }
        boolean flag = false;
        while (!flag){
            for (Future<String> future:list1){
                try {
                    String s = future.get();
                    if (s =="成功"){
                        flag = true;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("主线程继续执行222222222222222:::::"+Thread.currentThread().getName());

4.6. 知识小结

方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的.

5. 手写线程池的几种方式

5.1. 阿里巴巴为什么不建议直接使用Async注解?

导读:对于异步方法调用,从Spring3开始提供了@Async注解,该注解可以被标在方法上,以便

异步地调用该方法。调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor

的任务中,由指定的线程池中的线程执行。

在项目应用中,@Async调用线程池,推荐使用自定义线程池的模式。

自定义线程池常用方案:重新实现接口AsyncConfigurer

5.2. 应用场景

同步:同步就是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果。

异步:异步调用则是只是发送了调用的指令,调用者无需等待被调用的方法完全执行完毕;而是

继续执行下面的流程。

例如, 在某个调用中,需要顺序调用 A, B, C三个过程方法;如他们都是同步调用,则需要将他

们都顺序执行完毕之后,方算作过程执行完毕;如B为一个异步的调用方法,则在执行完A之

后,调用B,并不等待B完成,而是执行开始调用C,待C执行完毕之后,就意味着这个过程执行

完毕了。

在Java中,一般在处理类似的场景之时,都是基于创建独立的线程去完成相应的异步调用逻辑,

通过主线程和不同的业务子线程之间的执行流程,从而在启动独立的线程之后,主线程继续执行

而不会产生停滞等待的情况。

5.3. Spring 已经实现的线程池

  • SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
  • SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
  • ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
  • SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
  • ThreadPoolTaskExecutor:最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

5.4. 异步的方法

  • 最简单的异步调用,返回值为void
  • 带参数的异步调用,异步方法可以传入参数
  • 存在返回值,常调用返回Future
Spring中启用@Async

@Async应用默认线程池

Spring应用默认的线程池,指在@Async注解在使用时,不指定线程池的名称。

查看源码,@Async的默认线程池为SimpleAsyncTaskExecutor。

无返回值调用

基于@Async无返回值调用,直接在使用类,使用方法(建议在使用方法)上,加上注解。若需

要抛出异常,需手动new一个异常抛出。

有返回值Future调用

CompletableFuture并不使用@Async注解,可达到调用系统线程池处理业务的功能。

JDK5新增了Future接口,用于描述一个异步计算的结果。

虽然 Future 以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方

便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和我们的异步编程的初衷相

违背,轮询的方式又会耗费无谓的CPU 资源,而且也不能及时地得到计算结果。

  • CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段
  • 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。

比如:

stage.thenApply(x -> square(x)).thenAccept(x -> System.out.print(x)).thenRun(() -> System.out.println())
  • 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发

在Java8中,CompletableFuture 提供了非常强大的Future的扩展功能,可以帮助我们简化异步

编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转

换和组合 CompletableFuture 的方法。

  • 它可能代表一个明确完成的Future,也有可能代表一个完成阶段( CompletionStage ),它支持在计算完成以后触发一些函数或执行某些动作。
  • 它实现了Future和CompletionStage接口

默认线程池的弊端

在线程池应用中,参考阿里巴巴java开发规范:线程池不允许使用Executors去创建,不允许使

用系统默认的线程池,推荐通过ThreadPoolExecutor的方式,这样的处理方式让开发的工程师更

加明确线程池的运行规则,规避资源耗尽的风险。

Executors各个方法的弊端:

  • newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPool和newScheduledThreadPool:要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

@Async默认异步配置使用的是SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个

线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错

误。针对线程创建问题,

SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当

concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况

下,会不断创建新的线程来处理任务。

基于默认配置,SimpleAsyncTaskExecutor并不是严格意义的线程池,达不到线程复用的功能。

5.5. @Async应用自定义线程池

自定义线程池,可对系统中线程池更加细粒度的控制,方便调整线程池大小配置,线程执行异常

控制和处理。在设置系统自定义线程池代替默认线程池时,虽可通过多种模式设置,但替换默认

线程池最终产生的线程池有且只能设置一个(不能设置多个类继承AsyncConfigurer)

自定义线程池有如下模式:

  • 重新实现接口AsyncConfigurer
  • 继承AsyncConfigurerSupport
  • 配置由自定义的TaskExecutor替代内置的任务执行器

通过查看Spring源码关于@Async的默认调用规则,会优先查询源码中实现AsyncConfigurer这个

接口的类,实现这个接口的类为AsyncConfigurerSupport。但默认配置的线程池和异步处理方法

均为空,所以,无论是继承或者重新实现接口,都需指定一个线程池。

且重新实现 public Executor getAsyncExecutor()方法。

  • 实现接口AsyncConfigurer

  • 继承Reconfigurability

  • 配置自定义的TaskExecutor

由于AsyncConfigurer的默认线程池在源码中为空,Spring通过

beanFactory.getBean(TaskExecutor.class)先查看是否有线程池,未配置时,通过

beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME,

Executor.class),又查询是否存在默认名称为TaskExecutor的线程池。所以可在项目中,定义名

称为TaskExecutor的bean生成一个默认线程池。也可不指定线程池的名称,申明一个线程池,

本身底层是基于TaskExecutor.class便可。

比如:

Executor.class:ThreadPoolExecutorAdapter->ThreadPoolExecutor->AbstractExecutorService->ExecutorService->Executor

这样的模式,最终底层为Executor.class,在替换默认的线程池时,需设置默认的线程池名称为

TaskExecutor

TaskExecutor.class:ThreadPoolTaskExecutor->SchedulingTaskExecutor->AsyncTaskExecutor->TaskExecutor

这样的模式,最终底层为TaskExecutor.class,在替换默认的线程池时,可不指定线程池名称。

五、扩展:CompletionStage(待更新)

六、扩展:CompletableFuture(待更新)

标签:Java,Thread,System,任务,线程,new,多线程,public
From: https://blog.csdn.net/qq_51226710/article/details/141634278

相关文章

  • java计算机毕业设计员工信息管理系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着企业规模的不断扩大与业务复杂化,传统的人工管理方式在员工信息管理上逐渐显露出效率低下、数据易错、信息孤岛等问题。尤其是在当今快节奏的商业......
  • java计算机毕业设计在线论文投稿系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展,学术交流模式正经历着前所未有的变革。传统的论文投稿与审稿流程,往往受限于地域、时间及人为因素,导致效率低下、透明度不足......
  • java计算机毕业设计医院信息管理系统(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着医疗技术的飞速发展和医疗需求的日益增长,传统的手工医疗管理模式已难以满足现代医院高效、精准、便捷的管理要求。医院信息管理系统(HIS)的引入,旨......
  • Java之线程篇二
    目录Thread的常见构造方法Thread的常见属性代码示例1代码示例2示例代码3代码示例4代码示例5小结线程中断代码示例1代码示例2代码示例3代码示例4小结线程等待获取当前线程的引用Thread的常见构造方法举例Threadt1=newThread();Threadt2=newThr......
  • Java服务.问题排查.问题复现
    最近有用户反馈测试环境Java服务总在凌晨00:00左右挂掉,用户反馈Java服务没有定时任务,也没有流量突增的情况,Jvm配置也合理,莫名其妙就挂了问题排查问题复现为了复现该问题,写了个springboot的demo部署在测试环境,其中demo里只做了helloworld功能,应用类型为web_tomcat(war包部署),基......
  • Java替换RequstBody和RequestParam参数的属性
    Java替换RequstBody和RequestParam参数的属性本文主要讲解在Java环境中如何替换RequestBody和RequestParam参数中的属性背景近期由于接手的老项目中存在所有接口中新增一个加密串来给接口做一个加密效果(项目历史原因,不方便上Jwt授权这套),所以就研究了一下Http请求链路,发现可以......
  • Java 中的自增++和自减--运算符【小白必看】
    Java中的自增和自减运算符在学习Java编程语言时,自增(++)和自减(--)运算符是两个常见且基础的操作符。它们主要用于对变量进行简单的加减操作。以下我们将详细讲解这两个运算符的用法,并通过代码示例来帮助理解。1.自增运算符(++)自增运算符(++)用于将变量的值增加1。在Jav......
  • JavaScript中数组;JavaScript中对象及方法;笔记分享;知识回顾
    一,JS中数组数组创建4种语法:<!DOCTYPEhtml><html><head><metacharset="UTF-8"><title></title><script>/*第一种......
  • JavaScript中DOW和BOW;笔记分享;知识回顾
    一,BOW1什么是BOWBOM是BrowserObjectModel的简写,即浏览器对象模型。BOM有一系列对象组成,是访问、控制、修改浏览器的属性的方法BOM没有统一的标准(每种客户端都可以自定标准)。BOM的顶层是window对象2,window对象及常用方法(1),什么是window对象Window对象描述:    ......
  • 【Java】Record的使用 (简洁教程)
    Java系列文章目录补充内容Windows通过SSH连接Linux第一章Linux基本命令的学习与Linux历史文章目录Java系列文章目录一、前言二、学习内容:三、问题描述四、解决方案:4.1为什么引入Record4.2Record与Class区别4.3使用场景五、总结:5.1场景使用5.2字段的定义......