浅谈java开启异步线程的几种方法(@Async,AsyncManager,线程池)

Posted 天河归来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈java开启异步线程的几种方法(@Async,AsyncManager,线程池)相关的知识,希望对你有一定的参考价值。

浅谈java开启异步线程的几种方法

整体描述

在java中异步线程很重要,比如在业务流处理时,需要通知硬件设备,发短信通知用户,或者需要上传一些图片资源到其他服务器这种耗时的操作,在主线程里处理会阻塞整理流程,而且我们也不需要等待处理结果之后再进行下一步操作,这时候就可以使用异步线程进行处理,这样主线程不会因为这些耗时的操作而阻塞,保证主线程的流程可以正常进行。
最近在项目中使用了很多线程的操作,在这做个记录。

实现方法

线程的操作,是java中最重要的部分之一,实现线程操作也有很多种方法,这里仅介绍几种常用的。在springboot框架中,可以使用注解简单实现线程的操作,还有AsyncManager的方式,如果需要复杂的线程操作,可以使用线程池实现。下面根据具体方法进行介绍。

一、注解@Async

springboot框架的注解,使用时也有一些限制,这个在网上也有很多介绍,@Async注解不能在类本身直接调用,在springboot框架中,可以使用单独的Service实现异步方法,然后在其他的类中调用该Service中的异步方法即可,具体如下:

1. 添加注解

在springboot的config中添加 @EnableAsync注解,开启异步线程功能

package com.thcb.boot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

/**
 * MyConfig
 *
 * @author thcb
 */

@Configuration
@EnableAsync
public class MyConfig 
    // 自己配置的Config

2. 创建异步方法Service和实现类

使用service实现耗时的方法
Service类:

package com.thcb.execute.service;

import org.springframework.scheduling.annotation.Async;

/**
 * IExecuteService
 *
 * @author thcb
 */
public interface IExecuteService 

    /**
     * 一些耗时的操作,使用单独线程处理
     * 这里就简单写了一个sleep5秒的操作
     */
    @Async
    public void sleepingTest();


Service实现类:

package com.thcb.execute.service.impl;

import com.thcb.execute.service.IExecuteService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * ExecuteService业务层处理
 *
 * @author thcb
 */
@Service
public class ExecuteServiceImpl implements IExecuteService 

    private static final Logger log = LoggerFactory.getLogger(ExecuteServiceImpl.class);

    @Override
    public void sleepingTest() 
        log.info("SleepingTest start");
        try 
            Thread.sleep(5000);
         catch (Exception e) 
            log.error("SleepingTest:" + e.toString());
        
        log.info("SleepingTest end");
    


3. 调用异步方法

这里根据Springboot的框架,在controller层调用,并使用log查看是否时异步结果。
controller:

package com.thcb.boot.controller;

import com.thcb.execute.service.IExecuteService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * TestController
 *
 * @author thcb
 */
@RestController
public class TestController 

    private static final Logger log = LoggerFactory.getLogger(TestController.class);

    @Autowired
    private IExecuteService executeService;

    @RequestMapping("/test")
    public String test() 
        return "spring boot";
    

    @RequestMapping("/executeTask")
    public String executeTask() 

        log.info("executeTask Start!");
        executeService.sleepingTest();
        log.info("executeTask End!");
        return "executeTask";
    

在log查看结果:

接口直接返回了executeTask,并log出executeTask End!在5秒之后,log打出SleepingTest end,说明使用了异步线程处理了executeService.sleepingTest的方法。

二、AsyncManager

使用AsyncManager方法,也是SpringBoot框架中带的任务管理器,可以实现异步线程。

1. 创建AsyncManager类

使用AsyncManager首先需要创建一个AsyncManager类,这个在springboot框架中应该也是有的:

/**
 * 异步任务管理器
 *
 * @author thcb
 */
public class AsyncManager 
    /**
     * 操作延迟10毫秒
     */
    private final int OPERATE_DELAY_TIME = 10;

    /**
     * 异步操作任务调度线程池
     */
    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

    /**
     * 单例模式
     */
    private AsyncManager() 
    

    private static AsyncManager me = new AsyncManager();

    public static AsyncManager me() 
        return me;
    

    /**
     * 执行任务
     *
     * @param task 任务
     */
    public void execute(TimerTask task) 
        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
    

    /**
     * 停止任务线程池
     */
    public void shutdown() 
        Threads.shutdownAndAwaitTermination(executor);
    

2. 创建一个耗时的操作类

这里同样需要创建一个耗时的操作,也是用sleep模拟:

    public TimerTask sleepingTest() 
        return new TimerTask() 
            @Override
            public void run() 
            	// 耗时操作
            	try 
                	Thread.sleep(5000);
        		 catch (Exception e) 
            		log.error("SleepingTest:" + e.toString());
        		
            
        ;
    

3. 执行异步操作

使用AsyncManager执行异步操作也比较简单,直接调用即可:

	// 异步线程池
    AsyncManager.me().execute(sleepingTest());

三、线程池

使用线程池可以设定更多的参数,线程池在网上也有很多详细的介绍,在这我只介绍一种,带拒绝策略的线程池。

1. 创建线程池

创建带有拒绝策略的线程池,并设定核心线程数,最大线程数,队列数和超出核心线程数量的线程存活时间:

    /**
     * 线程池信息: 核心线程数量5,最大数量10,队列大小20,超出核心线程数量的线程存活时间:30秒, 指定拒绝策略的
     */
    private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(20), new RejectedExecutionHandler() 
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 
            log.error("有任务被拒绝执行了");
        
    );

2. 创建一个耗时的操作类

由于线程池需要传入一个Runnable,所以此类继承Runnable,还是用sleep模拟耗时操作。

    /**
     * 耗时操作
     */
    static class MyTask implements Runnable 
        private int taskNum;

        public MyTask(int num) 
            this.taskNum = num;
        

        @Override
        public void run() 
            System.out.println("正在执行task " + taskNum);
            try 
                Thread.sleep(4000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println("task " + taskNum + "执行完毕");
        
    

3. 执行线程池

开启线程池,这里通过一个for循环模拟一下,可以看一下log输出,有兴趣的可以修改一下for循环和sleep的数值,看看线程池具体的操作和拒绝流程。

        for (int i = 0; i < 20; i++) 
            MyTask myTask = new MyTask(i);
            threadPoolExecutor.execute(myTask);
            System.out.println("线程池中线程数目:" + threadPoolExecutor.getPoolSize() + ",队列中等待执行的任务数目:" +
                    threadPoolExecutor.getQueue().size() + ",已执行完别的任务数目:" + threadPoolExecutor.getCompletedTaskCount());
        
        threadPoolExecutor.shutdown();

总结

在此写一些线程操作需要注意的地方:

  1. 线程数量和cpu有关,使用线程时一定要注意线程的释放,否则会导致cpu线程数量耗尽;
  2. 使用注解完成的线程操作,不可以在自己的类中实现调用,因为注解最后也是通过代理的方式完成异步线程的,最好时在单独的一个service中写;
  3. 线程池最好单独写,使用static和final修饰,保证所有使用该线程池的地方使用的是一个线程池,而不能每次都new一个线程池出来,每次都new一个就没有意义了。

以上就是三种线程池的操作,写的不算很详细,有兴趣的同学可以自己在深入研究一下,还有Java8新加的CompletableFuture,可以单独写一篇文章了,在此篇就不再介绍了:)

字节架构师:来说说Java异步调用的几种方式

一、通过创建新线程【Java资料领取】

首先的我们得认识到,**异步调用的本质,其实是通过开启一个新的线程来执行。**如以下例子:

public static void main(String[] args) throws Exception{

    System.out.println("主线程 =====> 开始 =====> " + System.currentTimeMillis());

    new Thread(() -> {
        System.out.println("异步线程 =====> 开始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("异步线程 =====> 结束 =====> " + System.currentTimeMillis());
    }).start();

    Thread.sleep(2000);

    System.out.println("主线程 =====> 结束 =====> " + System.currentTimeMillis());

}

数据结果如下所示,我们知道,System.currentTimeMillis()时间单位为ms。

主线程 =====> 开始 =====> 1627893837146
异步线程 =====> 开始 =====> 1627893837200
主线程 =====> 结束 =====> 1627893839205
异步线程 =====> 结束 =====> 1627893842212

我们通过线程休眠来达成主线程执行时间2秒左右,异步线程执行5秒左右的效果。通过打印出来的时间戳倒数第四位(秒位)我们可以看出,两个的线程执行总时间为5秒左右,符合异步执行的特征

以上是采用Runable实现多线程创建方式的lambda写法,关于的lambda知识,可参考Java Lambda 表达式;而关于多线程的多种实现方式,Java多线程事务管理一文有提及,可移步查看

二、通过线程池

因为异步任务的实现本质的由新线程来执行任务,所以通过线程池的也可以实现异步执行。写法同我们利用线程池开启多线程一样。但由于我们的目的不是执行多线程,而是异步执行任务,所以一般需要另外一个线程就够了。

因此区别于执行多线程任务的我们常用的newFixedThreadPool,在执行异步任务时,我们用newSingleThreadExecutor 来创建一个单个线程的线程池。

public static void main(String[] args) throws Exception{

    System.out.println("主线程 =====> 开始 =====> " + System.currentTimeMillis());

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.submit(()->{
        System.out.println("异步线程 =====> 开始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("异步线程 =====> 结束 =====> " + System.currentTimeMillis());
    });
    executorService.shutdown(); // 回收线程池

    Thread.sleep(2000);

    System.out.println("主线程 =====> 结束 =====> " + System.currentTimeMillis());

}

执行结果如下:

主线程 =====> 开始 =====> 1627895467578
异步线程 =====> 开始 =====> 1627895467635
主线程 =====> 结束 =====> 1627895469644
异步线程 =====> 结束 =====> 1627895472649

可以看到,结果跟第一种结果是基本一致的。

温馨提示:不要忘记线程池的回收

三、通过@Async注解

我们都知道,SpringBoot项目有一个的很重要的特点就是的注解化。如果你的项目是SpringBoot,那就又多了一种选择——@Async注解。

使用起来也非常简单,将要异步执行的代码封装成一个方法,然后用@Async注解该方法,然后在主方法中直接调用就行。

@Test
public void mainThread() throws Exception{

    System.out.println("主线程 =====> 开始 =====> " + System.currentTimeMillis());
    collectionBill.asyncThread();
    Thread.sleep(2000);
    System.out.println("主线程 =====> 结束 =====> " + System.currentTimeMillis());

    Thread.sleep(4000); // 用于防止jvm停止,导致异步线程中断
}

@Async
public void asyncThread(){
    System.out.println("异步线程 =====> 开始 =====> " + System.currentTimeMillis());
    try{
        Thread.sleep(5000);
    }catch (InterruptedException e){
        e.printStackTrace();
    }
    System.out.println("异步线程 =====> 结束 =====> " + System.currentTimeMillis());
}

执行结果如下:

主线程 =====> 开始 =====> 1627897539948
异步线程 =====> 开始 =====> 1627897539956
主线程 =====> 结束 =====> 1627897541965
异步线程 =====> 结束 =====> 1627897544966

有以下两点需要注意:

  1. 类似@Tranctional注解,@Async注解的方法与调用方法不能在同一个类中,否则不生效
  2. JUnit框架的设计不考虑多线程场景,所以主线程退出后,子线程也会跟着立即退出,所以可以在后面加多线程休眠时间来观察异步线程的执行情况

四、通过CompletableFuture

CompletableFuture是JDK1.8的新特性,是对Future的扩展。CompletableFuture实现了CompletionStage接口和Future接口,增加了异步回调、流式处理、多个Future组合处理的能力。

实现代码如下:

public static void main(String[] args) throws Exception{

    System.out.println("主线程 =====> 开始 =====> " + System.currentTimeMillis());

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    CompletableFuture.runAsync(() ->{
        System.out.println("异步线程 =====> 开始 =====> " + System.currentTimeMillis());
        try{
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("异步线程 =====> 结束 =====> " + System.currentTimeMillis());
    },executorService);
    executorService.shutdown(); // 回收线程池

    Thread.sleep(2000);

    System.out.println("主线程 =====> 结束 =====> " + System.currentTimeMillis());

}

同样可以实现类似的结果如下:

主线程 =====> 开始 =====> 1627898354914
异步线程 =====> 开始 =====> 1627898354977
主线程 =====> 结束 =====> 1627898356980
异步线程 =====> 结束 =====> 1627898359979

2021年最新阿里P7 Java面试题流出,附完整答案详解,看完跳槽吊打面试官​

-End-

最近有一些小伙伴,让我帮忙找一些Java面试资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是Java面试必备!所有资料都整理到网盘了,欢迎下载!

需要的小伙伴,可以扫码领取

以上是关于浅谈java开启异步线程的几种方法(@Async,AsyncManager,线程池)的主要内容,如果未能解决你的问题,请参考以下文章

字节架构师:来说说Java异步调用的几种方式

字节架构师:来说说Java异步调用的几种方式

Android中使用异步线程更新UI视图的几种方法

异步编程的几种方式,你知道几种?

最近在研究多线程,浅谈JAVA中多线程的几种实现方式

异步的几种方法