天天看點

别問了,我真的不喜歡這個注解!

你好呀,我是why。

我之前寫過一些關于線程池的文章,然後有同學去翻了一圈,發現我沒有寫過一篇關于

@Async

注解的文章,于是他來問我:

别問了,我真的不喜歡這個注解!

是的,我攤牌了。

我不喜歡這個注解的原因,是因為我壓根就沒用過。

别問了,我真的不喜歡這個注解!

我習慣用自定義線程池的方式去做一些異步的邏輯,且這麼多年一直都是這樣用的。

是以如果是我主導的項目,你在項目裡面肯定是看不到

@Async

注解的。

那我之前見過

@Async

注解嗎?

肯定是見過啊,有的朋友就喜歡用這個注解。

一個注解就搞定異步開發,多爽啊。

我不知道用這個注解的人知不知道其原理,反正我是不知道的。

最近開發的時候引入了一個元件,發現調用的方法裡面,有的地方用到了這個注解。

既然這次用到了,那就研究一下吧。

首先需要說明的是,本文并不會寫線程池相關的知識點。

僅描述我是通過什麼方式,去了解這個我之前一無所知的注解的。

搞個 Demo

不知道大家如果碰到這種情況會去怎麼下手啊。

但是我認為不論是從什麼角度去下手的,最後一定是會落到源碼裡面的。

是以,我一般是先搞個 Demo。

Demo 非常簡單啊,就三個類。

首先是啟動類,這沒啥說的:

别問了,我真的不喜歡這個注解!

然後搞個 service:

别問了,我真的不喜歡這個注解!

這個 service 裡面的 syncSay 方法被打上了

@Async

注解。

最後,搞個 Controller 來調用它,完事:

别問了,我真的不喜歡這個注解!

Demo 就搭建好了,你也動手去搞一個,耗時超過 5 分鐘,算我輸。

然後,把項目啟動起來,調用接口,檢視日志:

别問了,我真的不喜歡這個注解!

我去,從線程名稱來看,這也沒異步呀?

怎麼還是 tomcat 的線程呢?

于是,我就遇到了研究路上的第一個問題:

@Async

注解沒有生效。

為啥不生效?

為什麼不生效呢?

我也是懵逼的,我說了之前對這個注解一無所知,那我怎麼知道呢?

那遇到這個問題的時候會怎麼辦?

當然是面向浏覽器程式設計啦!

這個地方,如果我自己從源碼裡面去分析為啥沒生效,一定也能查出原因。

但是,如果我面向浏覽器程式設計,隻需要 30 秒,我就能查到這兩個資訊:

失效原因:

  • 1.

    @SpringBootApplication

    啟動類當中沒有添加

    @EnableAsync

    注解。
  • 2.沒有走 Spring 的代理類。因為

    @Transactional

    @Async

    注解的實作都是基于 Spring 的 AOP,而 AOP 的實作是基于動态代理模式實作的。那麼注解失效的原因就很明顯了,有可能因為調用方法的是對象本身而不是代理對象,因為沒有經過 Spring 容器管理。

很顯然,我這個情況符合第一種情況,沒有添加

@EnableAsync

注解。

另外一個原因,我也很感興趣,但是現在我的首要任務是把 Demo 搭建好,是以不能被其他資訊給誘惑了。

很多同學帶着問題去查詢的時候,本來查的問題是

@Async

注解為什麼沒有生效,結果慢慢的就走偏了,十五分鐘後問題就逐漸演變為了 SpringBoot 的啟動流程。

再過半小時,網頁上就顯示的是一些面試必背八股文之類的東西...

我說這個意思就是,查問題就好好查問題。查問題的過程中肯定會由這個問題引發的自己更加感興趣的問題。但是,記錄下來,先不要讓問題發散。

這個道理,就和帶着問題去看源碼一樣,看着看着,可能連自己的問題是什麼都不知道了。

别問了,我真的不喜歡這個注解!

好了,說回來。

我在啟動類上加上該注解:

别問了,我真的不喜歡這個注解!

再次發起調用:

别問了,我真的不喜歡這個注解!

可以看到線程名字變了,說明真的就好了。

現在我的 Demo 已經搭好了,可以開始找角度去卷了。

從上面的日志我也能知道,在預設情況下有一個線程字首為

task-

的線程池在幫我執行任務。

說到線程池,我就得知道這個線程池的相關配置才放心。

那麼我怎麼才能知道呢?

先壓一壓

其實正常人的思路這個時候就應該是去翻源碼,找對應的注入線程池的地方。

而我,就有點不正常了,我懶得去源碼裡面找,我想讓它自己暴露到我的面前。

怎麼讓它暴露出來呢?

仗着我對線程池的了解,我的第一個思路是先壓一壓這個線程池。

壓爆它,壓的它處理不過來任務,讓它走到拒絕邏輯裡面去,正常來說是會抛出異常的吧?

别問了,我真的不喜歡這個注解!

于是,我把程式稍微改造了一下:

别問了,我真的不喜歡這個注解!

想的是直接來一波大力出奇迹:

别問了,我真的不喜歡這個注解!

結果...

它竟然...

照單全收了,沒有異常?

日志一秒打幾行,打的很歡樂:

别問了,我真的不喜歡這個注解!

雖然沒有出現我預想的拒絕異常,但是我從日志裡面還是看出了一點點端倪。

比如我就發現這個 taks 最多就到 8:

别問了,我真的不喜歡這個注解!

朋友們,你說這是啥意思?

是不是就是說這個我正在尋找的線程池的核心線程數的配置是 8 ?

什麼,你問我為什麼不能是最大線程數?

有可能嗎?

當然有可能。但是我 10000 個任務發過來,沒有觸發線程池拒絕政策,剛好把最大線程池給用完了?

也就是說這個線程池的配置是隊列長度 9992,最大線程數 8 ?

這也太巧合了且不合理了吧?

是以我覺得核心線程數配置是 8 ,隊列長度應該是

Integer.MAX_VALUE

為了證明我的猜想,我把請求改成了這樣:

别問了,我真的不喜歡這個注解!

num=一千萬。

通過 jconsole 觀察堆記憶體使用情況:

别問了,我真的不喜歡這個注解!

那叫一個飙升啊,點選【執行GC】按鈕也沒有任何緩解。

也從側面證明了:任務有可能都進隊列裡面排隊了,導緻記憶體飙升。

雖然,我現在還不知道它的配置是什麼,但是經過剛剛的黑盒測試,我有正當的理由懷疑:

預設的線程池有導緻記憶體溢出的風險。
别問了,我真的不喜歡這個注解!

但是,同時也意味着我想從讓它抛出異常,進而自己暴露在我面前的騷想法落空。

怼源碼

前面的思路走不通,老老實實的開始怼源碼吧。

我是從這個注解開始怼的:

别問了,我真的不喜歡這個注解!

點進這個注解之後,幾段英文,不長,我從裡面擷取到了一個關鍵資訊:

别問了,我真的不喜歡這個注解!

主要關注我畫線的地方。

In terms of target method signatures, any parameter types are supported.

在目标方法的簽名中,入參是任何類型都支援的。

多說一句:這裡說到目标方法,說到 target,大家腦海裡面應該是要立刻出現一個代理對象的概念的。

上面這句話好了解,甚至感覺是一句廢話。

但是,它緊跟了一個 However:

However, the return type is constrained to either void or Future.
别問了,我真的不喜歡這個注解!

constrained,受限制,被限制的意思。

這句話是說:傳回類型被限制為 void 或者 Future。

啥意思呢?

那我偏要傳回一個 String 呢?

别問了,我真的不喜歡這個注解!

WTF,列印出來的居然是 null !?

别問了,我真的不喜歡這個注解!

那這裡如果我傳回一個對象,豈不是很容易爆出空指針異常?

看完注解上的注釋之後,我發現了第二個隐藏的坑:

如果被

@Async

注解修飾的方法,傳回值隻能是 void 或者 Future。

void 就不說了,說說這個 Future。

看我劃線的另外一句:

it will have to return a temporary {@code Future} handle that just passes a value through: e.g. Spring's {@link AsyncResult}

上有一個 temporary,是四級詞彙啊,應該認識的,就是短暫的、暫時的意思。

temporary worker,臨時工,明白吧。

是以意思就是如果你要傳回值,你就用 AsyncResult 對象來包一下,這個 AsyncResult 就是 temporary worker。

就像這樣:

别問了,我真的不喜歡這個注解!

接着我們把目光放到注解的 value 屬性上:

别問了,我真的不喜歡這個注解!

這個注解,看注釋上面的意思,就是說這個應該填一個線程池的 bean 名稱,相當于指定線程池的意思。

也不知道了解的對不對,等會寫個方法驗證一下就知道了。

好了,到現在,我把資訊整理彙總一下。

  • 我之前完全不懂這個注解,現在我有一個 Demo 了,搭建 Demo 的時候我發現除了

    @Async

    注解之外,還需要加上

    @EnableAsync

    注解,比如加在啟動類上。
  • 然後把這個預設的線程池當做黑盒測試了一把,我懷疑它的核心線程數預設是 8,隊列長度無線長。有記憶體溢出的風險。
  • 通過閱讀

    @Async

    上的注解,我發現傳回值隻能是 void 或者 Future 類型,否則即使傳回了其他值,不會報錯,但是傳回的值是 null,有空指針風險。
  • @Async

    注解中有一個 value 屬性,看注釋應該是可以指定自定義線程池的。

接下來我把要去探索的問題排個序,隻聚焦到

@Async

的相關問題上:

  • 1.預設線程池的具體配置是什麼?
  • 2.源碼是怎麼做到隻支援 void 和 Future 的?
  • 3.value 屬性是幹什麼用的?

具體配置是啥?

我找到具體配置其實是一個很快的過程。

因為這個類的 value 參數簡直太友好了:

别問了,我真的不喜歡這個注解!

五處調用的地方,其中四處都是注釋。

有效的調用就這一個地方,直接先打上斷點再說:

org.springframework.scheduling.annotation.AnnotationAsyncExecutionInterceptor#getExecutorQualifier

發起調用之後,果然跑到了斷點這個地方:

别問了,我真的不喜歡這個注解!

順着斷點往下調試,就會來到這個地方:

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#determineAsyncExecutor
别問了,我真的不喜歡這個注解!

這個代碼結構非常的清晰。

編号為 ① 的地方,是擷取對應方法上的

@Async

注解的 value 值。這個值其實就是 bean 名稱,如果不為空則從 Spring 容器中擷取對應的 bean。

如果 value 是沒有值的,也就是我們 Demo 的這種情況,會走到編号為 ② 的地方。

這個地方就是我要找的預設的線程池。

最後,不論是預設的線程池還是 Spring 容器中我們自定義的線程池。

都會以方法為次元,在 map 中維護方法和線程池的映射關系。

也就是編号為 ③ 的這一步,代碼中的 executors 就是一個 map:

别問了,我真的不喜歡這個注解!

是以,我要找的東西,就是編号為 ② 的這個地方的邏輯。

這裡面主要是一個 defaultExecutor 對象:

别問了,我真的不喜歡這個注解!

這個玩意是一個函數式程式設計,是以如果你不知道這個玩意是幹什麼的,調試起來可能有點懵逼:

我建議你去惡補一下, 10 分鐘就能入門。

别問了,我真的不喜歡這個注解!

最終你會調試到這個地方來:

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#getDefaultExecutor
别問了,我真的不喜歡這個注解!

這個代碼就有點意思了,就是從 BeanFactory 裡面擷取一個預設的線程池相關的 Bean 出來。流程很簡單,日志也列印的很清楚,就不贅述了。

但是我想說的有意思的點是,我不知道你看到這份代碼,有沒有看出一絲絲雙親委派内味。

都是利用異常,在異常裡面處理邏輯。

就上面這“垃圾”代碼,直接就觸犯了阿裡開發規範中的兩大條:

别問了,我真的不喜歡這個注解!
别問了,我真的不喜歡這個注解!

在源碼裡面這就是好代碼。

在業務流程裡面,這就是違反了規範。

是以,說一句題外話。

就是阿裡開發規範我個人感覺,其實是針對我們寫業務代碼的同僚一個最佳實踐。

但是當把這個尺度拉到中間件、基礎元件、架構源碼的範圍時,就會出現一點水土不服的症狀,這個東西見仁見智,我是覺得阿裡開發規範的 idea 插件,對于我這樣寫增删查改的程式員來說,是真的香。

不說遠了,我們還是回來看看擷取到的這個線程池:

别問了,我真的不喜歡這個注解!

這不就找到我想要的東西了嗎,這個線程池的相關參數都可以看到了。

也證明了我之前猜想:

我覺得核心線程數配置是 8 ,隊列長度應該是 Integer.MAX_VALUE。

但是,現在我是直接從 BeanFactory 擷取到了這個線程池的 Bean,那麼這個 Bean 是什麼時候注入的呢?

朋友們,這還不簡單嗎?

我都已經拿到這個 Bean 的 beanName 了,就是 applicationTaskExecutor,但凡你把 Spring 擷取 bean 的流程的八股文背的熟練一點,你都知道在這個地方打上斷點,加上調試條件,慢慢去 Debug 就知道了:

org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String)
别問了,我真的不喜歡這個注解!

假設你就是不知道在上面這個地方打斷點去調試呢?

再說一個簡單粗暴的方法,你都拿到 beanName 了,在代碼裡面一搜不就出來了嘛。

簡單粗暴效果好:

org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
别問了,我真的不喜歡這個注解!

都找到這個類了,随便打個斷點,就可以開始調試了。

再說一個騷一點的操作。

假設我現在連 beaName 都不知道,但是我知道它肯定是一個被 Spring 管理的線程池。

那麼我就擷取項目裡面所有被 Spring 管理的線程池,總有一個得是我要找的吧?

你看下面截圖,目前這個 bean 不就是我要找的 applicationTaskExecutor 嗎?

别問了,我真的不喜歡這個注解!

這都是一些野路子,騷操作,知道就好,有時候多個排查思路。

别問了,我真的不喜歡這個注解!

傳回類型的支援

前面我們卷完了第一個關于配置的問題。

接下來,我們看另外一個前面提出的問題:

源碼是怎麼做到隻支援 void 和 Future 的?

答案就藏在這個方法裡面:

org.springframework.aop.interceptor.AsyncExecutionInterceptor#invoke
别問了,我真的不喜歡這個注解!

标号為 ① 的地方,其實就是我們前面分析的從 map 裡面拿 method 對應的線程池的方法。

拿到線程池之後來到标号為 ② 的地方,就是封裝一個 Callable 對象。

那麼是把什麼封裝到 Callable 對象裡面呢?

這個問題先按下不表,我們先牢牢的圍繞我們的問題往下走,不然問題會越來越多。

标号為 ③ 的地方,doSubmit,見名知意,這個地方就是執行任務的地方了。

org.springframework.aop.interceptor.AsyncExecutionAspectSupport#doSubmit
别問了,我真的不喜歡這個注解!

其實這裡就是我要找的答案。

你看這個方法的入參 returnType 是 String,其實就是被 @Async 注解修飾的 asyncSay 方法。

你要不信,我可以帶你看看前一個調用棧,這裡可以看到具體的方法:

别問了,我真的不喜歡這個注解!

怎麼樣,沒有騙你吧。

是以,現在你再看 doSubmit 方法拿着這個方法的傳回類型幹啥了。

一共四個分支,前面三個都是判斷是否是 Future 類型的。

其中的 ListenableFuture 和 CompletableFuture 都是繼承自 Future 的。

這個兩個類在 @Async 注解的方法注釋裡面也提到了:

别問了,我真的不喜歡這個注解!

而我們的程式走到了最後的一個 else,含義就是傳回值不是 Future 類型的。

那麼你看它幹了啥事兒?

别問了,我真的不喜歡這個注解!

直接把任務 submit 到線程池之後,就傳回了一個 null。

這可不得爆出空指針異常嗎?

到這個地方,我們也解決了這個問題:

源碼是怎麼做到隻支援 void 和 Future 的?

其實道理很簡單,我們正常的使用線程池送出不也就這兩個傳回類型嗎?

用 submit 的方式送出,傳回一個 Future,把結果封裝到 Future 裡面:

别問了,我真的不喜歡這個注解!

用 execute 的方式送出,沒有傳回值:

别問了,我真的不喜歡這個注解!

而架構通過一個簡單的注解幫我們實作異步化,它玩的再花裡胡哨 ,就算是玩出花來了,它也得遵守線程池送出的底層原理啊。

是以,源碼為什麼隻支援 void 和 Future 的傳回類型?

因為底層的線程池隻支援這兩種類型的傳回。

隻是它的做法稍微有點坑,直接把其他的傳回類型的傳回值都處理為 null 了。

你還别不服,誰叫你不讀注釋上的說明呀。

另外,我發現這個地方還有個小的優化點:

别問了,我真的不喜歡這個注解!

當它走到這個方法的時候,傳回值已經明确是 null 了。

為什麼還用

executor.submit(task)

送出任務呢?

用 execute 就行了啊。

差別,你問我差別?

不是剛剛才說了嗎, submit 方法是有傳回值的。

别問了,我真的不喜歡這個注解!

雖然你不用,但是它還是會去建構一個傳回的 Future 對象呀。

然而建構出來了,也沒用上呀。

是以直接用 execute 送出就行了。

少生成一個 Future 對象,算不算優化?

有一說一,不算什麼有價值的優化,但是說出去可是優化過 Spring 的源碼的,裝逼夠用了。

别問了,我真的不喜歡這個注解!

接着,再說一下我們前面按下不表的部分,這裡編号為 ② 的地方封裝的到底是什麼?

别問了,我真的不喜歡這個注解!

其實這個問題用腳指頭應該也猜到了:

别問了,我真的不喜歡這個注解!

隻是我單獨擰出來說的原因是我要給你證明,這裡傳回的 result 就是我們方法傳回的真實的值。

隻是判斷了一下類型不是 Future 的話就不做處理,比如我這裡其實是傳回了

hi:1

字元串的,隻是不符合條件,就被扔掉了:

别問了,我真的不喜歡這個注解!

另外,idea 還是很智能的,它會提示你這個地方的傳回值是有問題的:

别問了,我真的不喜歡這個注解!

甚至修改方法都給你标出來了,你隻需要一點,它就給你重新改好了。

别問了,我真的不喜歡這個注解!

對于為什麼要這麼改,現在我們已經拿捏的非常清楚了。

知其然,也知其是以然。

别問了,我真的不喜歡這個注解!

@Async 注解的 value

接下來我們看看 @Async 注解的 value 屬性是幹什麼的。

其實在前面我已經悄悄的提到了,隻是一句話就帶過了,就是這個地方:

别問了,我真的不喜歡這個注解!

前面說編号為 ① 的地方,是擷取對應方法上的

@Async

注解的 value 值。這個值其實就是 bean 名稱,如果不為空則從 Spring 容器中擷取對應的 bean。

然後我就直接分析到标号為 ② 的地方了。

現在我們重新看看标号為 ① 的地方。

我也重新安排一個測試用例去驗證我的想法。

反正 value 值應該是 Spring 的 bean 名稱,而且這個 bean 一定是一個線程池對象,這個沒啥說的。

是以,我把 Demo 程式修改為這樣:

别問了,我真的不喜歡這個注解!

再次跑起來,跑到這個斷點的地方,就和我們預設的情況不一樣了,這個時候 qualifier 有值了:

别問了,我真的不喜歡這個注解!

接下來就是去 beanFactory 裡面拿名字為 whyThreadPool 的 bean 了。

最後,拿出來的線程池就是我自定義的這個線程池:

别問了,我真的不喜歡這個注解!

這個其實是一個很簡單的探索過程,但是這背後蘊涵了一個道理。

就是之前有同學問我的這個問題:

别問了,我真的不喜歡這個注解!

其實這個問題挺有代表性的,很多同學都認為線程池不能濫用,一個項目共用一個就好了。

線程池确實不能濫用,但是一個項目裡面确實是可以有多個自定義線程池的。

根據你的業務場景來劃分。

比如舉個簡單的例子,業務主流程上可以用一個線程池,但是當主流程中的某個環節出問題了,假設需要發送預警短信。

發送預警短信的這個操作,就可以用另外一個線程池來做。

它們可以共用一個線程池嗎?

可以,能用。

但是會出現什麼問題呢?

假設項目中某個業務出問題了,在不斷的,瘋狂的發送預警短信,甚至把線程池都占滿了。

這個時候如果主流程的業務和發送短信用的是同一個線程池,會出現什麼美麗的場景?

是不是一送出任務,就直接走到拒絕政策裡面去了?

預警短信發送這個附屬功能,導緻了業務不可以,本末倒置的了吧?

是以,建議使用兩個不同的線程池,各司其職。

這其實就是聽起來很高大上的線程池隔離技術。

那麼落到

@Async

注解上是怎麼回事呢?

其實就是這樣的:

别問了,我真的不喜歡這個注解!

然後,還記得我們前面提到的那個維護方法和線程池的映射關系的 map 嗎?

就是它:

别問了,我真的不喜歡這個注解!

現在,我把程式跑起來調用一下上面的三個方法,目的是為了把值給放進去這個 map:

别問了,我真的不喜歡這個注解!

看明白了嗎?

再次複述一次這句話:

以方法次元維護方法和線程池之間的關系。

現在,我對于

@Async

這個注解算是有了一點點的了解,我覺得它也還是很可愛的。

後面也許我會考慮在項目裡面把它給用起來。

别問了,我真的不喜歡這個注解!

好了,本文的技術部分就到這裡啦。

下面這個環節叫做[荒腔走闆],技術文章後面我偶爾會記錄、分享點生活相關的事情,和技術毫無關系。我知道看起來很突兀,但是我喜歡,因為這是一個普通部落客的生活氣息。

你要不喜歡,退出之前記得文末點個“在看”哦。

荒腔走闆

前幾天突然發現之前我經常去逛的那個花鳥魚市場要拆遷了,我感覺我的快樂就要沒了。

剛剛過去的周末又去逛了一圈,看見幾個小多肉還挺别緻的,看上了眼。

拿在手裡問女朋友:你覺得這個多少錢一個?

女朋友說:至少得 10 元吧,還帶一個小陶瓷盆呢。

我看着我覺得差不多,因為平時至少是 15 元一盆了,拆遷嘛,再怎麼也得便宜一點。

于是準備講價到 10 元拿下。

問老闆,這個多少錢一個呀?

老闆答:8 元任選。

于是我和女朋友開心的選了三盆。

别問了,我真的不喜歡這個注解!

走在回家的路上,我們碰到了鄰居,發現他們手裡也提着三盆小多肉。

于是我嘴賤問了一句:你們這三盆多少錢呀?

他們回答說:3 盆一共 10 元。

那一刻,我的快樂又沒了。

随後,鄰居也問了我同樣的問題:你們這三盆多少錢呀?

當我說出 24 的時候,他們差點就沒忍住,笑出聲來。

這件事讓我明白了兩個道理。

一,以後買東西跟在别人後面,等别人買完之後,就上去說:我也來一個和他一樣的。

二,快樂來源于對比,痛苦也是。

最後說一句

好了,看到了這裡了,轉發、在看、點贊随便安排一個吧,要是你都安排上我也不介意。寫文章很累的,需要一點正回報。

給各位讀者朋友們磕一個了:

别問了,我真的不喜歡這個注解!

推薦👍 :我就辣雞怎麼了?

推薦👍 :我再說一次:我不是碼農,我是工程師!

推薦👍 :提心吊膽!我做支付系統時最害怕的幾個問題...

推薦👍 :一不小心,寫出10w+爆款,收益竟然這麼多...

推薦👍 :就這樣,我走完了程式員的前五年...