一、線程調度原理
在任意時刻,CPU只能執行一條指令,每個線程獲取到CPU的使用權之后才可以執行指令也就是說在任意時刻,只有一個線程占用CPU 處于運行狀態
多線程并發,實際上是指多個線程輪流獲取CPU 的使用權然后分別執行各自的任務,在可運行區中,其實有多個線程處于就緒狀態的線程在等待CPU?
而JVM 的一項任務,就是要負責線程的調度線程的調度就是按照特定的機制為多個線程來分配CPU的使用權,
二、線程調度模型
- 分時調度模型:它的思想是讓線程輪流獲取CPU 的使用權,并且平均每個線程占用CPU 的時間片
- 搶占式調度模型:Java虛擬機采用的模型,它的思想是優先讓可運行池中優先級高的線程來占用CPU ,如果運行池中的優先級都一樣,那就隨機選擇一個。如果程序想要干預運行順序,那就給每個線程設置一個優先級
三、Android線程調度
由兩個因素來決定:
- nice值
? ? ?在Process 中定義
? ? ?值越小,優先級越高
? ? ?默認值是:THREAD_PRIORITY_DEFAULT,0
? ? ?最低是19,后臺優先級是16
? ? ?可以設置成負數,優先級更高
? ? ?
如果后臺進程比較多,會影響前臺進程的運行,所以還需要另一種機制來處理這種特殊的情況
- cgroup?
借助Linux 的cgroup來執行更嚴格的前臺和后臺策略,后臺線程會被隱式的移到后臺group ,當其他組的線程處于工作狀態,那后臺group 的線程就會被限制,使用很小的幾率來利用CPU ,這種分離的調度策略既允許了后臺線程來執行一些任務,同時不會對用戶的前臺線程造成影響,保證前臺線程能使用更多的CPU?
哪些線程會被移到后臺group?
- 優先級設置的比較低的線程
- 不在前臺運行的應用程序的線程
注意點
- 線程過多會導致CPU 頻繁切換,降低線程運行效率異步不能無限制的使用
- 正確認識到線程執行任務的重要性,來決定哪種優先級,一般優先級與線程的工作量成反比
- 線程的優先級具有繼承性,比如在UI線程中直接創建一個子線程,它的優先級和UI 線程一樣高
四、異步方式匯總
Thread?
- ?最簡單,常見的異步方式
- 不易復用,頻繁創建及銷毀開銷大
- 復雜場景不易使用,比如要執行一個定時任務thread方式不方便使用
HandlerThread?
- ?本質是一個Thread ,自帶消息循環
- ?內部串行執行任務
- 比較適合那些需要長時間運行,不斷的從隊列中獲取任務的場景
IntentService?
- 內部實現:繼承自service 在內部創建HandlerThread ,繼承了HandlerThread 的特性
- 相對于service 來說,它的執行是異步的,不會阻塞UI線程的執行
- ?優先級較高,不會被系統kill?
AsyncTask?
- Android提供的異步工具類
- 內部實現是兩個線程池,一個handler
- 無需開發者去處理線程切換的問題
- 需注意版本不一致的問題,實現方式不一致,適配Android版本要在14以上
線程池
- Java提供的線程池
- 易復用,減少頻繁創建、銷毀的時間
- 功能強大:定時,任務隊列,并發控制等
RxJava?
- 由強大的Scheduler 集合提供
- 不同的類型區分,IO,Comptution?
- 如果項目中集成了RxJava 推薦使用RxJava的線程池
五、線程使用準則
- 嚴禁直接new Thread?
- 提供基礎線程池供各個業務線使用,避免各個業務線各自維護一套線程池,導致線程數過多
- 根據任務類型選擇合適的異步方式
? ? 優先級低,長時間執行,使用HandlerThread
? ? 定時執行,使用線程池
- 創建線程必須命名
? 方便定位線程歸屬
? 運行期,通過Thread.currentThread().setName()修改名字
- 對關鍵異步任務進行監控
? ? 異步不等于不耗時
? ? AOP的方式來做監控
- 重視優先級設置,Java的線程調度是一個搶占式的調度模型
? ?Process.setThreadPriority()
? ?可以設置多次
六、鎖定線程創建
- 項目變大以后收斂線程
- 項目源碼,三方庫,aar 中都有線程的創建
- 避免惡化的一種監控預防手段
解決方案
? ?分析:
- 創建線程的位置去獲取堆棧信息
- 所有的異步方式,都會走到new Thread?
- 由于有的源碼拿不到,不能直接在new Thread 的地方直接去獲取堆棧信息,此時特別適合采用Hook手段,在特定的方法之內注入自己的邏輯
? ? 找Hook 點,構造函數或者特定方法
? ? 在這里,就可以找Thread 的構造函數在構造方法里加入自己的邏輯,獲取調用棧信息拿到調用棧信息就能知道哪個調用不是調用我們自己的線程也可以知道調用棧信息是屬于哪一個業務方
DexposedBridge.hookAllConstructors(Thread. class, new XC_MethodHook(
@Overrideprotected void afterHookedMethod (MethodHookParam param) throw Throwable {super.afterHookedMethod (param);Thread thread = (Thread) param.thisObject;LogUtils.i ( msg: thread. getName ()+" stack "+Log.getStackTraceString(new throwable);
七、線程收斂
?常規方案:
- ?根據線程創建堆棧考量合理性,使用統一線程庫
- ?各個業務線需要下掉自己的線程庫使用統一的線程庫
基礎庫怎么使用線程
- ?直接依賴線程庫
- ?缺點:線程庫更新可能會導致基礎庫更新
優雅的實現
- 基礎庫內部暴露API setEeecutor 基礎庫只需要修改一次
- 初始化的時候注入統一的線程庫
統一線程庫
- 區分任務類型:IO、CPU密集型
- IO密集型任務不消耗CPU,核心池可以很大
- CPU密集型任務:核心池大小和CPU核心數相關
八、模擬問題
1.線程使用為什么會遇到問題?
- 在項目的初期階段,主要是關注業務功能,忽視了基礎庫的建設,具體到線程方面,沒有采用統一的線程池,每個地方使用線程的方式比較亂,同時線程數量比較多
- 在項目發展壯大之后,遇到了一些線程的性能問題,比如說主線程卡頓,以及一些異步任務執行非常耗時
- Java的線程調度是一個搶占式的調度模型線程優先級比較重要,但是項目中并沒有做這些區分,對IO和CPU密集型的任務也沒有做區分,很有可能主線程搶不到時間片的情況
2.怎么在項目中對線程進行優化?
- 首先針對于項目中線程數過多的這種情況,做了線程收斂,通過hook 手段來獲取每個線程運行的堆棧信息
- 然后結合業務場景來看這個線程是否需要單獨來創建通過這種方式,盡可能在業務層面將線程收斂到統一的線程庫當中,而對于基礎庫,每個基礎庫統一對外暴露一個接口,提供一個線程池實現的能力,在基礎庫使用之前來注入線程庫,這樣基礎庫都用到了線程庫
- 基礎線程庫針對于IO密集型任務和CPU密集型任務做區分,對于IO密集型任務,比如網絡請求,文件操作,它并不消耗CPU ,所以將核心池設置的比較大,而對于CPU密集型任務,如果核心數數量過高,他可能會導致CPU 頻繁調度,反而會導致執行效率下降,因此根據CPU核心數來決定CPU線程池核心數大小
- ?還做了其他處理,比如對重要的異步邏輯進行監控,監控他的執行時間,同時在執行異步任務的時候注重優先級以及線程名的設置