天天看點

Java 異步程式設計 (5 種異步實作方式詳解)

同步操作如果遇到一個耗時的方法,需要阻塞等待,那麼我們有沒有辦法解決呢?讓它異步執行,下面我會詳解異步及實作 @mikechen

什麼是異步?

首先我們先來看看一個同步的使用者注冊例子,流程如下:

Java 異步程式設計 (5 種異步實作方式詳解)

在同步操作中,我們執行到 插入資料庫 的時候,我們必須等待這個方法徹底執行完才能執行“ 發送短信 ”這個操作,如果 插入資料庫 這個動作執行時間較長,發送短信需要等待,這就是典型的同步場景。

于是聰明的人們開始思考,如果兩者關聯性不強,能不能将一些非核心業務從主流程中剝離出來,于是有了異步程式設計雛形,改進後的流程如下:

Java 異步程式設計 (5 種異步實作方式詳解)

這就是異步程式設計,它是程式并發運作的一種手段,它允許多個事件同時發生,當程式調用需要長時間運作的方法時,它不會阻塞目前的執行流程,程式可以繼續運作。

在聊完異步程式設計後,那麼我們一起來看看Java裡面實作異步程式設計究竟有哪些方式呢?

一、線程異步

在 Java 語言中最簡單使用異步程式設計的方式就是建立一個 線程來實作,如果你使用的 JDK 版本是 8 以上的話,可以使用 Lambda 表達式 會更加簡潔。

public class AsyncThread extends Thread{
    @Override
    public void run() {
        System.out.println("目前線程名稱:" + this.getName() + ", 執行線程名稱:" + Thread.currentThread().getName() + "-hello");
    }
}      
public static void main(String[] args) {

  // 模拟業務流程
  // .......

    // 建立異步線程 
    AsyncThread asyncThread = new AsyncThread();

    // 啟動異步線程
    asyncThread.start();
}      

當然如果每次都建立一個 Thread線程,頻繁的建立、銷毀,浪費系統資源,我們可以采用線程池:

private ExecutorService executor = Executors.newCachedThreadPool() ;

    public void fun() throws Exception {

        executor.submit(new Runnable(){

            @override

                public void run() {

                    try {
                     //要執行的業務代碼,我們這裡沒有寫方法,可以讓線程休息幾秒進行測試

                        Thread.sleep(10000);

                        System.out.print("睡夠啦~");

                    }catch(Exception e) {

                        throw new RuntimeException("報錯啦!!");

                    }

                }

        });

    }      

将業務邏輯封裝到 Runnable 或 Callable 中,交由 線程池 來執行。

二、Future異步

上述方式雖然達到了多線程并行處理,但有些業務不僅僅要執行過程,還要擷取執行結果,後續提供在JUC包增加了Future。

從字面意思了解就是未來的意思,但使用起來卻着實有點雞肋,并不能實作真正意義上的異步,擷取結果時需要阻塞線程,或者不斷輪詢。

@Test
public void futureTest() throws Exception {

    System.out.println("main函數開始執行");

    ExecutorService executor = Executors.newFixedThreadPool(1);
    Future<Integer> future = executor.submit(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {

            System.out.println("===task start===");
            Thread.sleep(5000);
            System.out.println("===task finish===");
            return 3;
        }
    });
    //這裡需要傳回值時會阻塞主線程,如果不需要傳回值使用是OK的。倒也還能接收
    //Integer result=future.get();
    System.out.println("main函數執行結束");

    System.in.read();

}      

三、CompletableFuture異步

Future 類通過 get() 方法阻塞等待擷取異步執行的運作結果,性能比較差。

JDK1.8 中,Java 提供了 CompletableFuture 類,它是基于異步函數式程式設計。相對阻塞式等待傳回結果,CompletableFuture 可以通過回調的方式來處理計算結果,實作了異步非阻塞,性能更優。

CompletableFuture 實作了 Future 和 CompletionStage 接口, 并提供了多種實作異步程式設計的方法,如supplyAsync, runAsync以及thenApplyAsync。

下面我們使用CompletableFuture來實作上面的例子:

CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
while (!completableFuture.isDone()) {
    System.out.println("CompletableFuture is not finished yet...");
}
long result = completableFuture.get();       

我們不需要顯式使用 ExecutorService,CompletableFuture 内部使用了 ForkJoinPool 來處理異步任務,這使得我們的代碼變的更簡潔。

四、SpringBoot @Async異步

在@Async注解之前,使用多線程需要使用JDK的原生方法,非常麻煩,當有了@Async之後就比較簡單了。

首先,使用 @EnableAsync 啟用異步注解:

@SpringBootApplication
@EnableAsync
public class StartApplication {

    public static void main(String[] args) {
        SpringApplication.run(StartApplication.class, args);
    }
}      

自定義線程池:

@Configuration
@Slf4j
public class ThreadPoolConfiguration {

    @Bean(name = "defaultThreadPoolExecutor", destroyMethod = "shutdown")
    public ThreadPoolExecutor systemCheckPoolExecutorService() {

        return new ThreadPoolExecutor(3, 10, 60, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(10000),
                new ThreadFactoryBuilder().setNameFormat("default-executor-%d").build(),
                (r, executor) -> log.error("system pool is full! "));
    }
}      

在異步處理的方法上添加注解 ​

​@Async​

​ ,當對  ​

​execute 方法​

​ 調用時,通過自定義的線程池  ​

​defaultThreadPoolExecutor​

​ 異步化執行   ​

​execute 方法​

@Service
public class AsyncServiceImpl implements AsyncService {

    @Async("defaultThreadPoolExecutor")
    public Boolean execute(Integer num) {
        System.out.println("線程:" + Thread.currentThread().getName() + " , 任務:" + num);
        return true;
    }

}      

用 @Async 注解标記的方法,稱為異步方法。在spring boot應用中使用 @Async 很簡單:

  • 調用異步方法類上或者啟動類加上注解 @EnableAsync
  • 在需要被異步調用的方法外加上 @Async
  • 所使用的 @Async 注解方法的類對象應該是Spring容器管理的bean對象;

五、Guava異步

Guava 提供了 ListenableFuture 類來執行異步操作

1.首先我們需要添加 guava 的maven依賴:
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.2-jre</version>
</dependency>      
ExecutorService threadpool = Executors.newCachedThreadPool();
ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool);
ListenableFuture<Long> guavaFuture = (ListenableFuture<Long>) service.submit(()-> factorial(number));
long result = guavaFuture.get();       

Java異步程式設計小結