实现异步编程的9种方式,你都知道嘛?

前言

大家好,我是田螺

我们日常开发的时候,经常说到异步编程 。比如说,在注册接口,我们在用户注册成功时,用异步发送邮件通知用户

那么,小伙伴们,你知道实现异步编程,一共有多少种方式吗? 我给大家梳理了9种~~

  • 使用Thread和Runnable
  • 使用Executors提供线程池
  • 使用自定义线程池
  • 使用Future和Callable
  • 使用CompletableFuture
  • 使用ForkJoinPool
  • Spring的@Async异步
  • 公众号捡田螺的小男孩 (有田螺精心原创的面试PDF)
  • github地址,感谢每颗star:github

1. 使用Thread和Runnable

Thread和Runnable是最基本的异步编程方式,直接使用Thread和Runnable来创建和管理线程。

arduino 复制代码
public class Test {
    public static void main(String[] args) {
        System.out.println("田螺主线程:" + Thread.currentThread().getName());
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("田螺异步线程测试:"+Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
    }
}

但是呢,日常工作中,不推荐直接使用Thread和Runnable,因为它有这些缺点:

  • 资源消耗大:每次创建新线程会消耗系统资源,频繁创建和销毁线程会导致性能下降。
  • 难以管理:手动管理线程的生命周期、异常处理、任务调度等非常复杂。
  • 缺乏扩展性:无法轻松控制并发线程的数量,容易导致系统资源耗尽。
  • 线程复用问题:每次任务都创建新线程,无法复用已有的线程,效率低下。

2.使用Executors提供线程池

针对Thread和Runnable的缺点,我们可以使用线程池呀,线程池主要有这些优点:

  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。

有些小伙伴说,我们可以直接使用Executors提供的线程池呀,非常快捷方便,比如:

diff 复制代码
- Executors.newFixedThreadPool
- Executors.newCachedThreadPool

简单demo代码如下:

arduino 复制代码
public class Test {
    public static void main(String[] args) {
        System.out.println("田螺主线程:" + Thread.currentThread().getName());
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        executor.execute(()->{
            System.out.println("田螺线程池方式,异步线程:" + Thread.currentThread().getName());
        });
    }
}
//  运行结果:
田螺主线程:main
田螺线程池方式,异步线程:pool-1-thread-1

3. 使用自定义线程池

使用使用Executors提供线程池,如newFixedThreadPool,虽然简单快捷,但是呢,它的阻塞队列十无界的!

newFixedThreadPool默认使用LinkedBlockingQueue作为任务队列,而LinkedBlockingQueue是一个无界队列(默认容量为Integer.MAX_VALUE)。如果任务提交速度远大于线程池处理速度,队列会不断堆积任务最终可能导致内存耗尽.

因此,我们一般推荐使用自定义线程池,来开启异步。

csharp 复制代码
public class Test {
    public static void main(String[] args) {
        // 自定义线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60, // 空闲线程存活时间
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(8), // 任务队列
                Executors.defaultThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        System.out.println("田螺主线程:" + Thread.currentThread().getName());
        executor.execute(() -> {
            try {
                Thread.sleep(500); // 模拟耗时操作
                System.out.println("田螺自定义线程池开启异步:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}
//输出
田螺主线程:main
田螺自定义线程池开启异步:pool-1-thread-1

4. 使用Future和Callable

有些小伙伴说,如果我们期望异步编程可以返回结果呢?

那我们可以使用Future和Callable 。Future和Callable是Java 5引入的,用于处理异步任务。Callable类似于Runnable,但它可以返回一个结果,并且可以抛出异常。Future表示异步计算的结果

简单使用demo:

arduino 复制代码
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException  {
        // 自定义线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60, // 空闲线程存活时间
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(8), // 任务队列
                Executors.defaultThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        System.out.println("田螺主线程:" + Thread.currentThread().getName());

        Callable<String> task = () -> {
            Thread.sleep(1000); // 模拟耗时操作
            System.out.println("田螺自定义线程池开启异步:" + Thread.currentThread().getName());
            return "Hello, 公众号:捡田螺的小男孩!";
        };

        Future<String> future = executor.submit(task);

        String result = future.get(); // 阻塞直到任务完成
        System.out.println("异步结果:"+result);
    }
}

运行结果:
田螺主线程:main
田螺自定义线程池开启异步:pool-1-thread-1
异步结果:Hello, 公众号:捡田螺的小男孩!

5. 使用CompletableFuture

CompletableFuture是Java 8引入的,提供了更强大的异步编程能力,支持链式调用、异常处理、组合多个异步任务等。

简单使用demo:

arduino 复制代码
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException  {
        // 自定义线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60, // 空闲线程存活时间
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(8), // 任务队列
                Executors.defaultThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        System.out.println("田螺主线程:" + Thread.currentThread().getName());

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000); // 模拟耗时操作
                System.out.println("田螺CompletableFuture开启异步:" + Thread.currentThread().getName());
                return "Hello, 公众号:捡田螺的小男孩!";
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello, 公众号:捡田螺的小男孩!";
        },executor);

        future.thenAccept(result -> System.out.println("异步结果:" + result));
        future.join();
    }
}

6. 使用ForkJoinPool

有些时候,我们希望开启异步,将一个大任务拆分成多个小任务(Fork),然后将这些小任务的结果合并(Join)。这时候,我们就可以使用ForkJoinPool啦~。

ForkJoinPool 是 Java 7 引入的一个线程池实现,专门用于处理分治任务。

  • 它的特点就是任务拆分(Fork)和结果合并(Join),以及工作窃取(Work-Stealing)。
  • ForkJoinPool 特别适合处理递归任务或可以分解的并行任务。

简单使用demo:

arduino 复制代码
public class Test {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool(); // 创建 ForkJoinPool
        int result = pool.invoke(new SumTask(1, 100)); // 提交任务并获取结果
        System.out.println("1 到 100 的和为: " + result);
    }

    static class SumTask extends RecursiveTask<Integer> {
        private final int start;
        private final int end;

        SumTask(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            if (end - start <= 10) { // 直接计算小任务
                int sum = 0;
                for (int i = start; i <= end; i++) sum += i;
                return sum;
            } else { // 拆分任务
                int mid = (start + end) / 2;
                SumTask left = new SumTask(start, mid);
                SumTask right = new SumTask(mid + 1, end);
                left.fork(); // 异步执行左任务
                return right.compute() + left.join(); // 等待左任务完成并合并结果
            }
        }
    }

}

7. Spring的@Async异步

Spring 提供了 @Async 注解来实现异步方法调用,非常方便。使用 @Async 可以让方法在单独的线程中执行,而不会阻塞主线程。

Spring @Async 的使用步骤其实很简单:

  • 启用异步支持:在 Spring Boot项目中,需要在配置类或主应用类上添加 @EnableAsync 注解。
  • 标记异步方法:在需要异步执行的方法上添加 @Async 注解。
  • 配置线程池:默认情况下,Spring 使用一个简单的线程池。如果需要自定义线程池,可以通过配置实现。

启用异步支持

less 复制代码
@SpringBootApplication
@EnableAsync // 启用异步支持
public class AsyncDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncDemoApplication.class, args);
    }
}

异步服务类

typescript 复制代码
@Service
public class TianLuoAsyncService {

    @Async // 标记为异步方法
    public void asyncTianLuoTask() {
        try {
            Thread.sleep(2000); // 模拟耗时操作
            System.out.println("异步任务完成,线程: " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

默认情况下,Spring 使用一个简单的线程池(SimpleAsyncTaskExecutor),每次调用都会创建一个新线程。因此,在使用Spring的@Async进行异步时,推荐使用自定义线程池。

如下:

scss 复制代码
@Configuration
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 核心线程数
        executor.setMaxPoolSize(20);  // 最大线程数
        executor.setQueueCapacity(50); // 队列容量
        executor.setThreadNamePrefix("AsyncThread-"); // 线程名前缀
        executor.initialize();
        return executor;
    }
}

然后,在 @Async 注解中指定线程池的名称:

typescript 复制代码
@Async("taskExecutor") // 指定使用自定义的线程池
public void asyncTianLuoTask() {
    // 方法逻辑
}

8. MQ实现异步

我们在提到MQ的时候,经常提到,它有异步处理、解耦、流量削锋 。 是的,MQ经常用来实现异步编程

简要代码如下:先保存用户信息,然后发送注册成功的MQ消息

typescript 复制代码
  // 用户注册方法
  public void registerUser(String username, String email, String phoneNumber) {
      // 保存用户信息(简化版)
      userService.add(buildUser(username,email,phoneNumber))
      // 发送消息
      String registrationMessage = "User " + username + " has registered successfully.";
      // 发送消息到队列
      rabbitTemplate.convertAndSend("registrationQueue", registrationMessage);
  }

消费者从队列中读取消息并发送短信或邮件

typescript 复制代码
@Service
public class NotificationService {

    // 监听消息队列中的消息并发送短信/邮件
    @RabbitListener(queues = "registrationQueue")
    public void handleRegistrationNotification(String message) {
        // 这里可以进行短信或邮件的发送操作
        System.out.println("Sending registration notification: " + message);

        // 假设这里是发送短信的操作
        sendSms(message);

        // 也可以做其他通知(比如发邮件等)
        sendEmail(message);
    }
  }

9.使用Hutool工具库的ThreadUtil

可以使用的是类似 Hutool 工具库中的 ThreadUtil,它提供了丰富的线程池管理和异步任务调度功能。

先引入依赖:

xml 复制代码
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.11</version> <!-- 请使用最新版本 -->
</dependency>

最简单的,可以直接使用ThreadUtil.execute执行异步任务

arduino 复制代码
public class Test {
    public static void main(String[] args) {
        System.out.println("田螺主线程");
        ThreadUtil.execAsync(
                () -> {
                    System.out.println("田螺异步测试:" + Thread.currentThread().getName());
                }
        );
    }
}
//输出
田螺主线程
田螺异步测试:pool-1-thread-1
相关推荐
小突突突1 天前
Spring框架中的单例bean是线程安全的吗?
java·后端·spring
iso少年1 天前
Go 语言并发编程核心与用法
开发语言·后端·golang
掘金码甲哥1 天前
云原生算力平台的架构解读
后端
码事漫谈1 天前
智谱AI从清华实验室到“全球大模型第一股”的六年征程
后端
码事漫谈1 天前
现代软件开发中常用架构的系统梳理与实践指南
后端
Mr.Entropy1 天前
JdbcTemplate 性能好,但 Hibernate 生产力高。 如何选择?
java·后端·hibernate
YDS8291 天前
SpringCloud —— MQ的可靠性保障和延迟消息
后端·spring·spring cloud·rabbitmq
无限大61 天前
为什么"区块链"不只是比特币?——从加密货币到分布式应用
后端
洛神么么哒1 天前
freeswitch-初级-01-日志分割
后端
蝎子莱莱爱打怪1 天前
我的2025年年终总结
java·后端·面试