Spring Boot執行緒安全指南

banq發表於2018-12-05
Spring控制器/服務/單單例是執行緒安全的嗎?
答案是它取決於作用域: 決定元件執行緒安全性的主要因素是其作用域Scope。

哪個Spring作用域是執行緒安全的?
為了回答這個問題,首先需要了解Spring何時建立新執行緒。

在基於servlet的標準Spring Web應用程式中,每個新的HTTP請求都會生成一個新執行緒。如果容器為特定請求建立一個新的bean例項,我們可以說這個bean是執行緒安全的

讓我們來看一下Spring中的作用域,並關注容器何時建立它們。

Spring單例執行緒安全嗎?
簡短的回答是:不
這是因為單例Bean的生命週期很長。這些bean可能會在來自不同使用者的許多HTTP請求中反覆使用。如果不使用@Lazy ,框架會在應用程式啟動時建立唯一的一個bean例項,並確保使用者會自動連線並重用相同的這個例項。只要容器存在,這個單例Bean例項一直會存在。

但框架並不控制單例的使用方式。如果兩個不同的執行緒同時執行單例的方法,則不能保證兩個呼叫都將同步並在能順序執行。(需要synchronize等鎖才能實現同步)

換句話說,您有責任確保您的程式碼在多執行緒環境中安全執行。Spring不會為你做這事。


請求級別作用域Request scope
如果你想確保你的bean是執行緒安全的,你應該使用@RequestScope,顧名思義,Spring將這種bean例項繫結到特定的Web請求。
這種bean例項不在多個執行緒之間共享,因此您不必關心併發。

但是等一下。

如果這種bean的併發很大,建立bean的新例項就比重用現有例項要慢。這時候,使用單例Bean,除非你有一個真正的用例場景可以使用RequestScope的bean。

會話級別作用域
Spring將會話bean與特定使用者關聯。當新使用者訪問您的應用程式時,將建立一個新的會話Bean例項,併為該使用者的所有請求重用該例項。

如您所知,某些使用者的請求可能是併發的。因此,會話bean不是執行緒安全的。它們的生命週期比請求作用域bean長。多個請求可以同時呼叫同一個會話bean。

prototype Bean
我把原型範圍作為最後討論的範圍,因為我們無法清楚地說它始終是執行緒安全的。Prototype的執行緒安全性取決於包含原型的bean的作用域。

只要使用者需要這個Bean的例項,Spring就會根據需要建立原型bean。(類似new object一樣呼叫一次建立一次);

想象一下,你的應用程式中有兩個bean。一個是單例Bean,第二個是請求作用域的bean。兩者都依賴於第三個原型的bean。

讓我們先考慮單例bean:因為單例不是執行緒安全的,所以對其原型方法的呼叫也可以同時執行。當多個執行緒共享單例時,Spring注入該單例的原型的單個例項也將被共享。

對於請求作用域的bean:Spring為每個Web請求建立此類元件的新例項。每個請求都繫結到一個單獨的執行緒。因此,請求bean的每個例項都獲得自己的原型bean例項。在這種情況下,您可以將原型視為執行緒安全的。

那麼Spring Web控制器是否是執行緒安全的?

這取決於這種控制器的作用域。

如果將控制器定義為預設的單例bean,則它不是執行緒安全的。將預設作用域更改為會話級別的,也不會使控制器安全。但是,請求作用域將使控制器bean安全地用於併發Web請求。

如果將控制器定義為原型bean,因為我們從不將控制器注入其他Bean,它們是我們應用程式的入口點。那麼當您將控制器定義為原型bean時,Spring的行為如何?

當您將控制器定義為原型時,Spring框架將為每個Web請求建立一個新例項。除非將它們注入不安全的作用域bean,否則可以將原型作用域的控制器視為執行緒安全的。

如何使任何Spring bean執行緒安全?
可以做的最好的辦法是解決訪問同步問題。
怎麼做?
使您的bean類變成無狀態。(banq注:又回到了EJB的無狀態bean和有態Bean,無狀態實際是不可變)

如果bean的方法執行不修改其例項的欄位屬性,則bean是無狀態的。

更改方法內的區域性變數是完全可以的,因為對方法的每次呼叫都會為這些變數分配記憶體。與在所有非靜態方法之間共享的例項欄位不同。

完美的無狀態bean沒有欄位,但你不會經常看到這樣的實用程式類。通常,您的bean有一些欄位。但是透過應用一些簡單的規則,您可以使任何bean無狀態且執行緒安全。

如何使Spring bean無狀態?
將所有bean欄位設定為final,以指示在bean欄位的生命週期中不應再次重新分配。


但是不要將欄位修改與重新分配混淆!使所有bean的欄位final不會使它成為無狀態。如果在執行時期間可以更改分配給bean的最終欄位的值,則此類bean仍然不是執行緒安全的。

比如使用final String, 無法更改String欄位的值,String類是不可變的,就像Integer,Boolean和其他原始包裝器一樣。在這種情況下,您還可以安全地使用基本型別。但是更復雜的物件如Collection,Map或自定義資料類呢?

對於像集合這樣的常見型別,您可以使用標準Java庫中可以找到的不可變實現。您可以使用Java 9中新增的工廠方法輕鬆建立不可變集合。如果您仍使用舊版本,請不要擔心。您還可以在Collections類中找到轉換方法,如unmodifiableList()

如果涉及自定義資料型別,則必須確保它們是不可變的。在Java中建立不可變類超出了本文的範圍。(banq注:業務型別儘量使用值物件)


有狀態Spring bean中的執行緒安全變數
無狀態bean聽起來像銀彈。但是,如果您已經擁有有狀態bean並且必須在其中一個欄位上同步訪問許可權呢?

在這種情況下,您有一個經典的Java問題,即對類欄位的併發修改訪問。Spring框架不會為您解決它。您需要選擇一種可能的解決方案:

  • synchronized 關鍵字和鎖定-此選項使您可以訪問同步的最大控制,但還需要更深入的瞭解在並行環境中使用的機制
  • 原子變數 - 您可以在Java標準庫中找到一小組執行緒安全型別。該包中的型別可以安全地用作共享有狀態bean中的欄位。
  • 併發集合 - 除了原子變數之外,Java還為我們提供了一些有用的集合,我們可以使用它們而不必擔心併發訪問問題。

但請注意:無論您選擇哪種方法,訪問同步始終會對效能產生影響。如果您有其他選擇,請儘量避免使用它。

在Spring元件中實現執行緒安全的方法​​​​​​​
正如我們已經討論過的,Spring本身並沒有解決併發訪問的問題。如果bean的範圍不是執行緒安全的,但其方法包含一些您總是希望安全執行的關鍵程式碼,請在該方法上使用synchronized關鍵字。


結論
我們需要知道Spring框架在多執行緒環境中的情況。必須自行提供執行緒安全性時的保障。

banq:其實可變資料或狀態都是儲存資料庫,如果將資料庫作為業務核心,就不必擔心多執行緒問題,但是六邊形和乾淨架構中,需要將資料庫作為技術放到業務核心之外,在這種架構下,就需要多注意多執行緒問題。在普通場景下多執行緒問題基本由資料庫技術解決了。本文問題只適合作為面試問答。
 

相關文章