Java執行緒池和Spring非同步處理高階篇

程式設計師xiaozhang發表於2023-03-15

開發過程中我們會遇到很多使用執行緒池的場景,例如非同步簡訊通知,非同步發郵件,非同步記錄操作日誌,非同步處理批次Excel解析。這些非同步處理的場景我們都可以把它放線上程池中去完成,當然還有很多場景也都可以使用執行緒池,掌握執行緒池後開發中自己靈活應用。

例如在生成訂單的時候給使用者傳送簡訊,生成訂單的結果不應該被髮送簡訊的成功與否所左右,也就是說生成訂單這個主操作是不依賴於傳送簡訊這個操作,我們就可以把傳送簡訊這個操作置為非同步操作。當然也有的小夥伴會說我使用多執行緒不就行了,為啥還要使用執行緒池,那我就先聊一下執行緒和執行緒池的優缺點。

使用執行緒的缺點:

1:每次new Thread物件的時候,新建物件這樣效能很差。

2:執行緒缺乏管理,有可能無限建立執行緒,這樣可能造成系統資源的浪費或者OOM(記憶體溢位)。

使用執行緒池的優點:

1:重用存在的執行緒,減少執行緒的建立,效能良好。

2:可以有效的控制最大的執行緒併發數,提高系統資源的利用率。

說完上面就知道使用執行緒池有多好了吧,那知道了執行緒池的好處,我們怎樣使用執行緒池呢?好了重點物件出現了【PS 物件出現了汪汪汪?】。

這個時候可能會有小夥伴疑問為什麼要先聊執行緒池呢?Spring的非同步處理寫的很好直接用不就完事了,因為執行緒池和Spring的非同步處理有著千絲萬縷的關係,仔細看就知道了。

Java中使用執行緒池,那就要深刻理解大名鼎鼎的ThreadPoolExecutor物件。那怎麼建立這個物件的,請看給的原始碼So Easy (學會了建立物件,同事再不擔心你的學習能力了,廣告詞)

/**
     * Creates a new {@code ThreadPoolExecutor}
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

很多人一看就是運用幾個引數建立物件,確實不難。但是這幾個引數的表達的意思懂嗎,看英文確實有點不懂,好了那我就仔細聊聊這幾個引數,繼續學英語【這是真正學英語,不是電視劇中的學英語】

1:corePoolSize,執行緒池中的核心執行緒,當提交一個新的任務時候,執行緒池會建立一個新的執行緒執行任務,直到當前的執行緒數等於corePoolSize;如果當前執行緒數為corePoolSize,繼續提交新的任務到阻塞佇列中,等待被執行。

2:maximumPoolSize,執行緒池中允許的最大的執行緒數,如果阻塞佇列滿了,繼續提交新的任務,則建立新的執行緒執行任務。前提是當前執行緒數小於maximumPoolSize。

3:keepAliveTime,執行緒池維護執行緒所允許的時間,當執行緒池中的數量大於corePoolSize時候,如果沒有任務提交,核心執行緒外的執行緒不會被立即銷燬,而是等待時間超過了keepAliveTime。

4:unit,keepAliveTime的時間單位。

5:workQueue:用來儲存等待被執行任務的阻塞佇列,且任務必須實現Runable介面。

6:threadFactory,他是threadFactory型別的變數,用來建立執行緒,預設使用Executors.defaultThreadFactory()來建立執行緒。

7:handler:執行緒池的飽和策略,當阻塞佇列滿了,且沒有空閒的工作執行緒,如果繼續提交任務,必須採取一種策略處理該任務,執行緒池提供了4種策略:
7.1、AbortPolicy:直接丟擲異常,預設策略;
7.2、CallerRunsPolicy:用呼叫者所在的執行緒來執行任務;
73、DiscardOldestPolicy:丟棄阻塞佇列中靠最前的任務,並執行當前任務;
7.4、DiscardPolicy:直接丟棄任務;
上面的4種策略都是ThreadPoolExecutor的內部類。
當然也可以根據應用場景實現RejectedExecutionHandler介面,自定義飽和策略。

好了看到上面的解釋應該比較懂了吧,如果不懂那我再畫一張圖,幫你更好的理解執行緒池的工作原理,如下圖:

看了上圖如果還不懂,那我就給你上個程式碼,保證你看懂了【PS因為我剛開始就給你說了So Easy,不騙你】(呸呸呸,咋有點渣)。

public class ThreadPoolTest {

    public static void main(String[] args) {
        ThreadPoolExecutor pools = createPool();
        int activeCount = -1;
        int queueSize = -1;
        while (true) {
            if (activeCount != pools.getActiveCount() || queueSize != pools.getQueue().size()) {
                System.out.println("活躍的執行緒的個數:" + pools.getActiveCount());
                System.out.println("佇列中執行緒的個數:" + pools.getQueue().size());
                System.out.println("最大的執行緒的個數" + pools.getMaximumPoolSize());
                activeCount = pools.getActiveCount();
                queueSize = pools.getQueue().size();
                System.out.println("=========================================");
            }
        }
    }

     // 建立執行緒池,透過更改執行緒池的引數方便你更好的理解執行緒池
     // 其中第六個引數你也可以改成ThreadPoolExecutor預設的:Executors.defaultThreadFactory()
    private static ThreadPoolExecutor createPool() {
        ThreadPoolExecutor pools = new ThreadPoolExecutor(1, 2, 30,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1),
                r -> {
                    Thread t = new Thread(r);
                    return t;
                }, new ThreadPoolExecutor.AbortPolicy());
        System.out.println("The PoolExecutor is create done");
        pools.execute(() -> {
            sleep(100);
        });
        //這個裡面就可以寫自己的業務
        pools.execute(() -> {
            sleep(10);
        });
        pools.execute(() -> {
            sleep(10);
        });
        return pools;
    }
    private static void sleep(int seconds) {
        try {
            System.out.println("  " + Thread.currentThread().getName() + "   ");
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

好了理解了執行緒池,那就引入本文的重點Spring的非同步處理。如果是使用Spring Boot專案那隻需要2個註解就能搞定了。如下:

第一步加@EnableAsync註解,如下圖:

第二步在要使用的方法上加@Async註解,如下:

然後就可以直接使用了,如下是執行結果,加了Async註解和沒加註解出來的名字不一樣,有興趣的小夥伴可以試一下沒加註解列印出來的是什麼名字:

當然可能使用Spring Boot版本不同,列印出來的執行緒名稱可能會有點不同。這個時候可能會有小夥伴說這使用也太簡單了,講上面的執行緒池沒有啊。

繼續看,容我仔細說。我們先看下面2個註解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
  /**
   * Indicate the 'async' annotation type to be detected at either class
   * or method level.
   * 預設情況下,要開啟非同步操作,要在相應的方法或者類上加上@Async註解
   */
  Class<? extends Annotation> annotation() default Annotation.class;

  /**
   * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
   * to standard Java interface-based proxies.
   *   true表示啟用CGLIB代理
   */
  boolean proxyTargetClass() default false;

  /**
   * Indicate the order in which the {@link AsyncAnnotationBeanPostProcessor}
   * should be applied.
   * 直接定義:它的執行順序(因為可能有多個@EnableXXX)
   */
  int order() default Ordered.LOWEST_PRECEDENCE;

}

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Async {

  /**
   * A qualifier value for the specified asynchronous operation(s).
   *   這個value值是用來指定執行器的
   */
  String value() default "";
}

最重要的還是上面的@Import註解匯入的類:AsyncConfigurationSelector。這種方式我以前的文章說過很多次了,如果看過我以前寫的文章的,對這種匯入應該很熟悉,所以我直接說這個類的作用了。這個類幫我們匯入了ProxyAsyncConfiguration這個類,然後又幫我們注入了AsyncAnnotationBeanPostProcessor這個類。它就是和@Async比較相關的一個類了。從上的原始碼可議看出,支援@Asycn註解非同步處理我們寫的業務處理方法,交給了AnnotationAsyncExecutionInterceptor。具體的實現功能交給了它的繼承類AsyncExecutionInterceptor。由於主要功能處理都在AsyncExecutionInterceptor這個類中所以我主要聊這個類了。

首先是這個方法:

@Override
  @Nullable
  // 見名知意就知道這個是獲取執行緒池的方法,
  // 這個厲害了。如果父類返回的defaultExecutor 為null,
  // 那就new一個SimpleAsyncTaskExecutor作為預設的執行器,所以我們上文中
  // 如果沒有指定執行緒池,那麼就預設給我們一個預設的:SimpleAsyncTaskExecuto
protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
    Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
    return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
  }

先簡單說一下這個預設的執行緒池,看完這個預設的執行緒池解釋就知道我最開始為什麼要先說一下執行緒池了。

SimpleAsyncTaskExecutor:非同步執行使用者任務的SimpleAsyncTaskExecutor。每次執行使用者提交給它的任務時,它會啟動新的執行緒,並允許開發者控制併發執行緒的上限(concurrencyLimit),從而起到一定的資源節流作用。預設時,concurrencyLimit取值為-1,即不啟用資源節流,所以它不是真的執行緒池,這個類不重用執行緒,每次呼叫都會建立一個新的執行緒(因此建議我們在使用@Aysnc的時候,自己配置一個執行緒池,節約資源)

然後看獲取預設執行緒池的方法,這個方法很牛,先看程式碼後面解釋為什麼牛?【PS裡面中文都是新增的,老外們不會中文】。

protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
    if (beanFactory != null) {
      // 這個處理很有意思,它是用用的try catch的技巧去處理的
      try {
        // 如果容器記憶體在唯一的TaskExecutor(子類),就直接返回了
        return beanFactory.getBean(TaskExecutor.class);
      }
      catch (NoUniqueBeanDefinitionException ex) {
        // 這是出現了多個TaskExecutor型別的話,那就按照名字去拿  `taskExecutor`且是Executor型別
        try {
          return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
        }
        // 如果再沒有找到,也不要報錯,而是接下來建立一個預設的處理器
        // 這裡輸出一個info資訊
        catch (NoSuchBeanDefinitionException ex2) {
        }
      }
      catch (NoSuchBeanDefinitionException ex) {
        try {
          return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
        }
        catch (NoSuchBeanDefinitionException ex2) {
        }
        // 這裡還沒有獲取到,就放棄。用本地預設的executor吧~~~
        // 子類可以去複寫此方法,發現為null的話可議給一個預設值~~~~比如`AsyncExecutionInterceptor`預設給的就是`SimpleAsyncTaskExecutor`作為執行器的
        // Giving up -> either using local default executor or none at all...
      }
    }
    return null;
  }

好了看了獲取預設執行緒池的方法了,對我們後面配置程式中自己的執行緒池就有了很大的幫助了,慢慢知道我一開始為啥要先聊執行緒池了嗎,放心不會騙你的【PS呸呸呸 這句話說的是不是有點渣)

然後我們再聊執行緒非同步執行的方法如下:

這個三個步驟就是執行非同步的核心我會一個一個說:

determineAsyncExecutor方法

/**
   * Determine the specific executor to use when executing the given method.
   * Should preferably return an {@link AsyncListenableTaskExecutor} implementation.
   * @return the executor to use (or {@code null}, but just if no default executor is available)
   */
  @Nullable
  protected AsyncTaskExecutor determineAsyncExecutor(Method method) {
  // 如果快取中能夠找到該方法對應的執行器,就立馬返回了
    AsyncTaskExecutor executor = this.executors.get(method);
    if (executor == null) {
      Executor targetExecutor;
      // 抽象方法:AnnotationAsyncExecutionInterceptor有實現。
      // 就是@Async註解的value值
      String qualifier = getExecutorQualifier(method);
      // 現在知道@Async直接的value值的作用了吧。就是制定執行此方法的執行器的(容器內執行器的Bean的名稱)
      // 當然有可能為null。注意此處是支援@Qualified註解標註在類上來區分Bean的
      // 注意:此處targetExecutor仍然可能為null
      // 使用自定義執行緒池d額時候Async註解的value值最好加上執行緒池的名稱
      if (StringUtils.hasLength(qualifier)) {
        targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
      }
      else {
        targetExecutor = this.defaultExecutor.get();
      }
      if (targetExecutor == null) {
        return null;
      }
      executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?
          (AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));
      this.executors.put(method, executor);
    }
    return executor;
  }

好了上面的程式碼中提到AnnotationAsyncExecutionInterceptor這個類的getExecutorQualifier方法了,這個方法也是極其重要的點,所以我直接拉出來,如下:

/**
   * Return the qualifier or bean name of the executor to be used when executing the
   * given method, specified via {@link Async#value} at the method or declaring
   * class level. If {@code @Async} is specified at both the method and class level, the
   * method's {@code #value} takes precedence (even if empty string, indicating that
   * the default executor should be used preferentially).
   */
  @Override
  @Nullable
  protected String getExecutorQualifier(Method method) {
    // Maintainer's note: changes made here should also be made in
    // AnnotationAsyncExecutionAspect#getExecutorQualifier
    // 可以見它就是去方法拿到@Async的value值。
    // 這下知道配置這個註解的作用了吧。
    Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class);
    if (async == null) {
      async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class);
    }
    return (async != null ? async.value() : null);
  }

根據這個@Async的配置,會得到具體的Executor,也就是執行緒池如下:

好了那獲取到非同步執行的執行緒池,那就開始執行具體的方法了,這個不聊了也就是我們寫的業務方法。執行完方法後幹嘛呢?當然就是第三步驟處理返回值啊,如下:

/**
   * Delegate for actually executing the given task with the chosen executor.
   *  用選定的執行者實際執行給定任務
   */
  @Nullable
  protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
   //根據不同的返回值型別,來採用不同的方案去非同步執行,但是執行器都是executor
    if (CompletableFuture.class.isAssignableFrom(returnType)) {
      return CompletableFuture.supplyAsync(() -> {
        try {
          return task.call();
        }
        catch (Throwable ex) {
          throw new CompletionException(ex);
        }
      }, executor);
    }
    // // ListenableFuture介面繼承自Future  是Spring自己擴充套件的一個介面。
    else if (ListenableFuture.class.isAssignableFrom(returnType)) {
      return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
    }
    // 普通的submit
    else if (Future.class.isAssignableFrom(returnType)) {
      return executor.submit(task);
    }
    else {
    // 沒有返回值的情況下  也用sumitt提交,按時返回null
      executor.submit(task);
      return null;
    }

一共四個分支,前面三個都是判斷是否是 Future 型別的。而我們的程式走到了最後的一個 else,含義就是如果返回值不是 Future 型別的。直接把任務 submit 到執行緒池之後,就返回了一個 null。這可不得爆出空指標異常嗎?但是原始碼為什麼只支援 void 和 Future 的返回型別?

因為底層的執行緒池只支援這兩種型別的返回。只是Spring的做法稍微有點坑,直接把其他的返回型別的返回值都處理為 null 了。

好了Spring處理非同步的過程都說了,我們也看到Spring的非同步處理器不是太好,需要我們自己配置預設的執行緒池,還有如果程式中有返回結果一定要記得把返回結果用Futrue封裝一下,要不然寫出來的程式可能出現空指標的情況【PS你已經是一個成熟的開發了,要記得自己避免空指標。嘿嘿又一句廣告詞】。

那我就把Spring非同步處理程式最佳化一點,自定義自己的非同步的執行緒池如下圖,不貼程式碼了,這個很重要要不要複製粘了自己多敲點程式碼吧:

結果:

PS :使用自定義執行緒池的時候@Async註解的value記得加上執行緒池的名稱,但是執行緒池不能濫用,但是一個專案裡面是可以有多個自定義執行緒池的。根據你的業務場景來劃分。比如舉個簡單的例子,業務主流程上可以用一個執行緒池,但是當主流程中的某個環節出問題了,假設需要傳送預警簡訊。傳送預警簡訊的這個操作,就可以用另外一個執行緒池來做。

執行緒池的那些引數具體配置多少,需要自己根據伺服器的配置,訪問的使用者量等等其他的一些資訊來進行配置,我只是讓大家理解執行緒池參數列達的意義,讓大家自己配置執行緒池引數更加方便。

相關文章