高併發問題拋去架構層面的問題,落實到代碼層面就是多線程的問題。多線程的問題主要是線程安全的問題(其餘還有活躍性問題,性能問題等)。java
那什麼是線程安全?下面這個定義來自《Java併發編程實戰》,這本書強烈推薦,是幾個Java語言的做者合寫的,都是併發編程方面的大神。編程
線程安全指的是:當多個線程訪問某個類時,這個類始終都能表現出正確的行爲。安全
正確指的是「所見即所知」,程序執行的結果和你所預想的結果一致。服務器
理解線程安全的概念很重要,所謂線程安全問題,就是處理對象狀態的問題。若是要處理的對象是無狀態的(不變性),或者能夠避免多個線程共享的(線程封閉),那麼咱們能夠放心,這個對象多是線程安全的。當沒法避免,必需要共享這個對象狀態給多線程訪問時,這時候纔用到線程同步的一系列技術。多線程
這個理解放大到架構層面,咱們來設計業務層代碼時,業務層最好作到無狀態,這樣就業務層就具有了可伸縮性,能夠經過橫向擴展平滑應對高併發。架構
因此咱們處理線程安全能夠有幾個層次:併發
1. 可否作成無狀態的不變對象。無狀態是最安全的。app
2. 可否線程封閉異步
3. 採用何種同步技術高併發
我理解爲可以「逃避」多線程問題,能逃則逃,實在不行了再來處理。
瞭解了線程封閉的背景,來講說線程封閉的具體技術和思路
1. 棧封閉
2. ThreadLocal
3. 程序控制線程封閉
棧封閉說白了就是多使用局部變量。理解Java運行時模型的同窗都知道局部變量的引用是保持在線程棧中的,只對當前線程可見,其餘線程不可見。因此局部變量是線程安全的。
ThreadLocal機制本質上是程序控制線程封閉,只不過是Java自己幫忙處理了。來看Java的Thread類和ThreadLocal類
1. Thread線程類維護了一個ThreadLocalMap的實例變量
2. ThreadLocalMap就是一個Map結構
3. ThreadLocal的set方法取到當前線程,拿到當前線程的threadLocalMap對象,而後把ThreadLocal對象做爲key,把要放入的值做爲value,放到Map
4. ThreadLocal的get方法取到當前線程,拿到當前線程的threadLocalMap對象,而後把ThreadLocal對象做爲key,拿到對應的value.
[java] view plain copy
ThreadLocal的設計很簡單,就是給線程對象設置了一個內部的Map,能夠放置一些數據。JVM從底層保證了Thread對象之間不會看到對方的數據。
使用ThreadLocal前提是給每一個ThreadLocal保存一個單獨的對象,這個對象不能是在多個ThreadLocal共享的,不然這個對象也是線程不安全的。
Structs2就用了ThreadLocal來保存每一個請求的數據,用了線程封閉的思想。可是ThreadLocal的缺點也顯而易見,必須保存多個副本,採用空間換取效率。
程序控制線程封閉,這個不是一種具體的技術,而是一種設計思路,從設計上把處理一個對象狀態的代碼都放到一個線程中去,從而避免線程安全的問題。
有不少這樣的實例,Netty5的EventLoop就採用這樣的設計,咱們的遊戲後臺處理用戶請求是也採用了這種設計。
具體的思路是這樣的:
1. 把和用戶狀態相關的代碼放到一個隊列中去,由一個線程處理
2. 考慮是否隔離用戶之間的狀態,即一個用戶使用一個隊列,仍是多個用戶使用一個隊列
拿Netty舉例,EventLoop被設計成了一個線程的線程池。咱們知道線程池的組成是工做線程 + 任務隊列。EventLoop的工做線程只有一個。
用戶請求過來後被隨機放到一個EventLoop去,也就是放到EventLoop線程池的任務隊列,由一個線程來處理。而且處理用戶請求的代碼都使用Pipeline職責鏈封裝好了,一個Pipeline交給一個線程來處理,從而保證了跟同一個用戶的狀態被封閉到了一個線程中去。
更多Netty EventLoop相關的內容看這篇 Netty5源碼分析(二) -- 線程模型分析
這裏有個問題也顯而易見,就是若是把多個用戶都放到一個隊列,交給一個線程處理,那麼前一個用戶的處理速度會影響到後一個用戶被處理的時間。
咱們的遊戲服務器的設計採用了一個用戶一個任務隊列的方式,處理任務的代碼被作成了Runnable,這樣多個Runnable能夠交給一個線程池執行,從而多個用戶能夠同時被處理,而同一個用戶的狀態處理被封閉到了惟一的一個任務隊列中,互不干擾。
可是也有問題,即線程池內的工做線程和任務隊列是有界的,因此單個線程處理的時間必需要快,不然大量請求被積壓在任務隊列來不及處理,一旦任務隊列也滿了,那麼後續的請求都進不來了。
若是使用無界的任務隊列,全部請求能進來,可是問題是高併發狀況下大量請求過來,會把系統內存撐爆,倒置OOM。
因此一個經常使用的設計思路以下:
1. 採用有界的任務隊列和不限個數的工做線程,這樣能夠平滑地處理高併發,不至於內存被撐爆
2. 單個線程請求時間必需要快,儘可能不超過100ms
3. 若是單個線程處理的時間因爲任務太大必須耗時,那麼把任務拆個小任務來屢次執行
4. 拆成小任務仍是慢,那麼把同步操做變成異步操做,即方法執行後當即返回,不要等待結果。由另外一個線程異步地處理線程,好比採用單獨的線程定時檢查處理狀態,或者採用異步回調的方式