多线程的几种创建方式以及手写@Async异步注解

一、多线程的应用场景

在我们的web开发中,其实多线程的场景非常多,而且对我们程序执行的效率有着很明显的提升,所以在什么场景下使用多线程是非常重要的。

使用多线程一定提高效率吗 ?

答: 不一定。我们首先需要了解CPU的调度算法,使用多线程需要进行一个我们常听到的但是不怎么理解的概念 – 上下文切换。 所谓上下文切换就是把前一个任务线程的当前执行信息和状态保存到程序计数器或者是CPU寄存器中,然后加载新任务线程的上下文信息,执行新线程任务。

如果我们创建几十个几百个线程来执行任务,那么我们CPU的上下文切换就会非常频繁,耗时也会增加,这时候说不定还不如单线程效率高。

我们使用多线程的场景有以下几点:
(1)客户端(移动App端)的开发
(2)异步发送通知(短信或者邮件)
(3)将比较耗时的代码改用为多线程异步执行,提高接口的响应速度(如耗时的下载操作)
(4)异步写入日志(一个操作可能产生很多条日志信息,同步将会影响程序的性能)

二、多线程的创建方式

多线程的创建方式有以下几种,我们将分别举例:

(1)继承Thread类创建线程
(2)实现Runnable接口创建线程(使用匿名内部类、lambda表达式)
(3)使用Callable和Future创建线程
(4)使用线程池例如用Executor框架
(5)spring @Async异步注解 结合线程池

(1)继承Thread类创建线程

/**
 * 线程创建方式1: 通过继承Thread接口
 *      启动线程调用的是start方法而不是run方法,run方法是调用start方法后自动调用的
 */
public class ThreadThread extends Thread {
    
    
    /**
     * 线程执行的代码,就是在run方法中
     */
    @Override
    public void run() {
    
    

    }
}

(2)实现Runnable接口创建线程(使用匿名内部类、lambda表达式)

/**
 * 线程创建方式2: 实现Runnable接口创建线程
 */
public class ThreadRunnable implements Runnable{
    
    
    /**
     * 线程执行的代码,就是在run方法中
     */
    @Override
    public void run() {
    
    }

    public static void main(String[] args) {
    
    
        /**
         * 使用匿名内部类的方式创建线程
         */
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    }
        });

        /**
         * 使用lambda方式创建线程
         */
        new Thread(() -> {
    
    });
    }
}

(3)使用Callable和Future创建线程

/**
 * 线程创建方式3: 实现Callable接口和Future接口创建线程
 */
public class ThreadCallable implements Callable<Integer> {
    
    

    @Override
    public Integer call() throws Exception {
    
    
        System.out.println(Thread.currentThread().getName() + "业务处理");
        return 10;
    }

    /**
     * 线程的使用
     * @param args
     */
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        ThreadCallable callable = new ThreadCallable();
        FutureTask<Integer> task = new FutureTask<>(callable);
        new Thread(task).start();
        // 底层通过juc包下的LockSupport.park() 和LockSupport.unpark()实现线程阻塞
        Integer result = task.get();
        System.out.println(result);
    }
}

(4)使用线程池例如用Executor框架

/**
 * 线程创建方式4: 通过线程池方式创建线程
 */
public class ThreadPoolThread {
    
    
    public static void main(String[] args) {
    
    
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
    
    
            threadPool.execute(new Runnable() {
    
    
                @Override
                public void run() {
    
    

                }
            });
        }
    }
}

(5)spring @Async异步注解 结合线程池

Spring提供了@Async来使用异步方法。

在spring boot应用中使用@Async很简单:

1、启动类加上@EnableAsync

2、在需要被异步调用的方法外加上@Async

三、手写@Async注解

我们可以尝试模仿Spring中提供的@Async注解来自己手写一个类似于这种异步工作的注解。
我们使用自定义注解 + AOP前置通知的 形式来实现该注解。

首先我们自定一个注解,如下:

//Target注解决定MyAnnotation注解可以加在哪些成分上,如加在类身上,或者属性身上,或者方法身上等成分
@Target({
    
    ElementType.TYPE, ElementType.METHOD})
//Retention注解决定自定义注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
public @interface ZalAsync {
    
    
}

然后我们再自定义一个注解处理Bean,如下:

@Aspect  // 注解功能通过AOP是西安
@Component // 将该处理Bean交给spring管理
@Slf4j
public class ZalAsyncAop {
    
    

    /**
     * 前置AOP通知,首先我们不使用多线程,观察测试类的执行顺序
     * @param joinPoint
     */
    @Around(value = "@annotation(com.example.springbootdemo.async.ZalAsync)")
    public void around(ProceedingJoinPoint joinPoint) {
    
    
        try {
    
    
       		// 目标方法的调用 
            joinPoint.proceed();
        } catch (Throwable e) {
    
    
            throw new RuntimeException(e);
        }
    }
}

写一个测试Controller和Service

@RequestMapping
@RestController
@Slf4j
public class TestController {
    
    

    @Autowired
    private AsyncService asyncService;

    @RequestMapping("/test")
    public void test() {
    
    
        log.info("<1>执行操作一");
        asyncService.asyncMethod();
        log.info("<3>执行操作三");
    }
}
@Component
@Slf4j
public class AsyncService {
    
    

    @ZalAsync
    public void asyncMethod() {
    
    
        try {
    
    
        	// 模拟耗时业务处理
            Thread.sleep(3000);
            log.info("<2>执行耗时方法二");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

我们启动springboot项目,访问localhost:8080/test,查看日志打印的情况,如下:

在这里插入图片描述
可以看出,整个方法执行至少需要3s,而耗时业务处理占据了整个方法的大部分时间,这里我们就可以使用多线程进行优化,注意到,方法被我们自定义注解@ZalAsync标注了,所以我们可以在自定义注解处理中新开一个线程进程处理。

优化代码如下:

@Aspect
@Component
@Slf4j
public class ZalAsyncAop {
    
    

    /**
     * 前置AOP通知
     * @param joinPoint
     */
    @Around(value = "@annotation(com.example.springbootdemo.async.ZalAsync)")
    public void around(ProceedingJoinPoint joinPoint) {
    
    
        new Thread(() -> {
    
     //新启动一个线程去执行耗时业务处理
            try {
    
    
                // 目标方法的调用
                joinPoint.proceed();
            } catch (Throwable e) {
    
    
                throw new RuntimeException(e);
            }
        }).start();
    }
}

重新启动项目,运行截图如下:
在这里插入图片描述
可以看到,普通的业务执行是由nio-8080-exec-1线程执行的,而较为耗时的业务处理则是由Thread-2线程执行的,并且日志输出的顺序也是不一样的,至此,我们就完成了自定义手写@Async异步注解。

当然这只是一个简单的多线程的应用和自定义注解的案例,这只是个人学习过程和自己对多线程应用的一些理解,欢迎各位大佬提出改进方案!

猜你喜欢

转载自blog.csdn.net/z318913/article/details/127693604