小白科普:“無狀態”那點事兒
軟件大師正在閉目修煉, 最小的一名弟子慢慢走了進來。
弟子:大師,弟子有一事不明,甚是煩惱。
大師:說來聽聽,讓為師給你排解一下。
弟子:我經常聽師兄們爭論‘無狀態’, 說‘無狀態’在軟件編程中是好事情, 可是到底什么是狀態? 什么是無狀態?
大師睜開眼來,寫下一行字: y=f(x),然后又閉上了眼睛。
弟子:(奇怪地問道)這不就是一個函數嗎?我初中就學過, 給定一個x,函數經過計算(比如求平方)就能得到一個y。
大師:沒錯,這就是一個純函數,對于相同的輸入,總是得到相同的輸出,不依賴于外界的狀態。
弟子:這也沒什么啊!
大師:你想想,要是有多個線程在一個CPU上并發調用這個函數,會不會有問題?
弟子:不會。
大師:如果是有多個線程在多個CPU上并行執行這個函數,會不會有問題?
弟子:不會。
大師:為什么?
弟子:因為每次調用都不會在這個函數中保留數據, 調用完了就完了,每一次調用都是嶄新的調用,并且***次和***百次之間沒有任何關系。
大師:因為那個函數不保存狀態,所以無論是并發還是并行,都沒有問題。
弟子:嗯,明白。
大師:你再想想你常用的HTTP,每次訪問一個靜態HTML頁面的時候,對于服務器來講,是不是就相當于調用了一個函數,函數輸入:一個URL路徑, 函數輸出:HTML頁面。
弟子:那這么說來,這個服務器也不會記錄每次請求的是誰,只要執行這個'函數調用'就可以了。
大師:你說說,這樣的HTTP協議有什么好處?
弟子:由于沒有狀態,如果一個服務器訪問量過大,我可以輕松地添加新的服務器來處理請求。
大師:“孺子可教也,這就是所謂水平擴展(scale-out)。
弟子:水平擴展? 難道還有垂直擴展(scale-up)?
大師:對,垂直擴展就是通過增加CPU,內存,硬盤等方式來提高單個服務器的處理能力。由于單臺機器總是有上限的,所以想應對海量用戶的訪問,提高可用性,還得靠水平擴展。現在你體會到無狀態的好處了吧?
弟子:明白了,大師,在服務器端無狀態確實是個美好的世界, 可是現實很殘酷,沒有狀態不行啊,一個人登錄了,我們得記住他是誰吧,他往購物車里加入商品,我們也得記下來吧。
大師:那你們怎么記啊?
弟子:肯定用Session來保存狀態??!
大師:服務器一旦引入狀態,就沒法輕松地水平擴展了吧!
弟子:是的,該怎么辦?
大師:這里邊辦法很多,例如讓'狀態'在各個服務器之間進行復制,但最常用的是把狀態轉移存儲到另外一個地方,盡量服務器恢復到無狀態的'y=f(x)'。
(注:實際情況下,圖中服務器之前還有負責負載均衡的服務器)
弟子:奧,這樣一來,又可以水平擴展了! 對了大師,我剛才聽到師兄們提到‘無狀態對象’,他們說就是一個對象沒有實例變量,或者實例變量是final的。這么說對吧?
大師:嗯,這種情況下,說‘無狀態對象’ 有點不準確了,更準確的詞是‘不可變對象’(Immutable Object),比如:
- public final class Complex{
- private final int a;
- private final int b;
- public Complex(int a, int b){
- this.a = a;
- this.b = b;
- }
- public Complex add(Complex other){
- return new Complex(a + other.a, b+other.b);
- }
- }
弟子:奧,這個類的對象一旦創建,就不能再改變了, 我看到了那個add方法,它不是對現有對象的修改,而是返回了一個全新的對象。
大師:這樣的話當多個線程調用add對象的時候,都是線程安全的。 我這里有一副圖畫,是LISP大師送給我的,形象地展示了可變 vs 不可變, 你拿去吧:
弟子:那代價也有點大啊,每次都創建新對象!我們用Spring,其中的Controller, Service被大量地并發調用,肯定不能用這種方法了。
大師:是的,你們用的Controller, Service 默認都是單例,運行期只有一個實例,他們的方法應該是y=f(x)這樣的無狀態方法,輕易不要在里邊放置共享的實例變量,要不然多線程并發操作就可能出問題了。
弟子:可是我們的Controller 一般都要放個Service的實例變量啊 ,比如這個LoginController中的userService, 多個線程同時訪問這個共享的userService,豈不就出問題了?
- @Controller
- public class LoginController {
- @Autowired
- private UserService userService;
- ......對userService的使用略......
- }
大師:你誤入歧途了,把無狀態和無共享的實例變量畫了等號,你想想,如果LoginController調用的userService 的方法也是類似 y=f(x), 會有線程安全問題嗎?
弟子:嗯...... 好像是沒有問題。 無論是Controller還是Service都是純函數調用而已。 但是如果確實需要共享的變量該怎么辦?
大師:很簡單,使用ThreadLocal,把這個變量存到各個線程當中,讓他們互不干擾,就線程安全了。
【本文為51CTO專欄作者“劉欣”的原創稿件,轉載請通過作者微信公眾號coderising獲取授權】