程式碼優化一下,用執行緒池管理那些隨意創建出來的執行緒

語言: CN / TW / HK

執行緒大家一定都用過,專案當中一些比較耗時的操作,比如網路請求,IO操作,我們都會把這類操作放在子執行緒中進行,因為如果放在主執行緒中,就會多少造成一些頁面卡頓,影響效能,不過是不是放在子執行緒中就好了呢,我們看看下面這段程式碼

image.png

很簡單的一段程式碼,建立了一個Thread,然後把耗時工作放在裡面進行就好了,如果專案當中只有一兩處出現這樣的程式碼,倒也影響不大,但是現在的專案當中,耗時的操作一大堆,比如檔案讀取,資料庫的讀取,sp操作,或者需要頻繁從某個伺服器獲取資料顯示在螢幕上,比如k線圖等,像這些操作如果我們都去通過建立新的執行緒去執行它們,那麼對效能以及記憶體的開銷是很大的,所以我們在平時開發過程當中應該養成習慣,不要去建立新的執行緒而是通過使用執行緒池去執行自己的任務

執行緒池

為什麼要使用執行緒池呢?執行緒池總結一下有以下幾點優勢 - 降低資源消耗:通過複用之前建立過的執行緒資源,降低執行緒建立與銷燬帶來的效能與記憶體的開銷 - 提高響應速度:無需等待執行緒建立,直接可以執行任務 - 提高執行緒可管理性:使用執行緒池可以對執行緒資源統一調優,分配,管理 - 使用更多擴充套件功能:使用執行緒池可以進行一些延遲或者週期性工作

而我們建立執行緒池的方式有以下幾種 - Executors.newFixedThreadPool:建立一個固定大小的執行緒池 - Executors.newCachedThreadPool:建立一個可快取的執行緒池 - Executors.newSingleThreadExecutor:建立單個執行緒數的執行緒池 - Executors.newScheduledThreadPool:建立一個可以執行延遲任務的執行緒池 - Executors.newSingleThreadScheduledExecutor:建立一個單執行緒的可以執行延遲任務的執行緒池 - Executors.newWorkStealingPool:建立一個搶佔式執行的執行緒池 - ThreadPoolExecutor:最原始的建立方式,以上六種方式的內部也是通過這個建立的執行緒池

雖然我們提倡使用執行緒池,但是有這麼多的建立方式,我們如果不在專案當中做一下管理的話,那麼各種各樣的執行緒池都有可能被使用到,由於每種建立方式對於執行緒的管理方式都不一樣,如果不合理建立的話,很可能會出現問題,所以我們需要有一個統一建立執行緒池的地方

統一管理執行緒

image.png

首先我們先建立一個執行緒池,使用Executors.newCachedThreadPool()去建立一個 ExecutorService,至於為什麼選擇newCachedThreadPool(),我們看下它的原始碼

image.png

從上面一大段英文註釋中我們能知道,這是一個可快取的執行緒池,並且corePoolSize為0說明這個執行緒池沒有始終存活的執行緒,如果執行緒池中沒有可用執行緒,會重新建立新執行緒,而執行緒池中如果有可用執行緒,那麼這個執行緒會被再利用,一個執行緒如果60秒內沒有被使用,那麼將會從佇列中移除並銷燬,所以個人感覺對於併發要求不是特別高的移動端,從效能角度來講使用這樣的一個執行緒池是比較合適的,當然具體設計方案以業務性質來決定,現在我們可以將專案當中的執行緒放在我們的執行緒池裡面運行了,再增加一個執行執行緒的函式

image.png

通過這個函式就可以有效的避免專案當中隨意建立執行緒的現象發生,讓專案當中的執行緒可以井然有序的執行,但是這還沒完事,我們知道Runnable在任務執行完成之後是沒有返回結果的,因為Runnable介面中的run方法的返回型別是個void,但實際開發當中,我們的確有需求,在執行一些比如查詢資料庫,讀取檔案之類的操作中,需要獲取任務的執行結果,之前都是通過線上程當中手動新增一個handler將需要的資料傳遞出來,再專業一點使用RxJava或者Flow,但不管什麼方式,這些都會造成程式碼耦合,我們還有更簡單的方式

Callable和Future

這兩個類是在java1.5之後推出來的,目的就是解決執行緒執行完成之後沒有返回結果的問題,我們先來對比下Runnable與Callable

image.png

相比較於Runnable,Callable接口裡面也有一個call的方法,這個方法是有返回值的,並且可以丟擲異常,所以以後當我們需要獲取任務的執行結果的時候,我們還可以使用Callable代替Runnable,那麼如何使用並獲取返回值呢?當然是使用我們已經建立好的ExecutorService,它裡面提供了一個函式去執行Callable

image.png

使用submit函式就可以執行我們的Callable,返回值是一個Future,而如何去獲取真正的返回結果,就在Future裡面,我們看下

image.png

使用get方法就可以獲取執行緒的執行結果,我們現在就來試試Callable和Future,在PoolManager裡面再增加一個函式,用來執行Callable

image.png

我們這裡有個簡單的任務,就是返回一段文字,然後將這段文字顯示在介面上,那麼第一步,先在佈局檔案裡面新增一個按鈕

image.png

然後點選這個按鈕,將任務裡面返回的文字顯示在按鈕上,程式碼如下

image.png

得到的效果如下

aa2.gif

在這邊特地把執行結果放在介面上而不是用日誌打印出來的原因可能大家已經發現了,Callable在返回執行結果的同時,也幫我們把執行緒切回到了主執行緒,所以我們不用在特地去切換執行緒更新ui介面了

週期性任務

普通的單個任務我們講完了,但是在專案當中往往會存在一些比較特殊的任務,可能需要你去週期性的去執行,舉個常見的例子,在證券類的app裡繪製k線圖的時候,並不需要將伺服器吐出來的資料統統拿出來繪製ui,這樣對效能的開銷是很大的,我們正確的做法是將資料都先存放在一個buffer裡面,然後定時的去buffer裡面拿最新資料就好,那這樣一個定時重新整理的功能如何在我們的執行緒池裡面去實現呢,這裡就要用到剛剛說到的另一種建立執行緒池的方式

image.png

這個函式建立的是一個ScheduledExecutorService物件,可週期性的執行任務,入參的corepoolSize表示可併發的執行緒數,現在我們在PoolManager裡面新增上這個ScheduledExecutorService

image.png

而如何去執行任務,我們使用ScheduledExecutorService裡面的scheduleAtFixedRate函式,我們先看下這個函式都有哪些入參

image.png

不用去看註釋我們就能知道怎麼使用這個函式,command就是執行的任務,第二,第三個引數分別表示延遲執行的時間以及任務執行的週期時間,第四個引數是時間的單位,在看返回值是一個ScheduleFuture,既然也是個Future,那是不是也可以通過它去獲取任務執行的結果呢?答案是拿不到的,一個原因是command是一個Runnable而不是Callable,不會返回任務的執行結果,另外我們從註釋上就能瞭解,這個ScheduleFuture只是用來當週期任務中有任務被取消了,或者被異常終止的時候,丟擲異常用的,那ScheduledExecutorService一定有入參是Callable的函式的吧,找了找發現並沒有,那只有一個辦法了,我們在command裡面去執行一個Callable任務,再將任務的執行結果回調出來就好了,程式碼設計如下

image.png

我們建立了一個函式叫executeScheduledJob,也有四個入參,job是一個Callable,用來執行我們的任務,callback是一個回撥,用來將任務執行結果回撥到上層去處理,後面兩個剛剛已經介紹過了,這裡設定了預設值,可自定義,現在我們就來實現一個簡單的讀秒功能,點選剛剛那個按鈕,按鈕初始值是1,然後每秒鐘加一,程式碼實現如下

image.png

這邊建立了一個CounterViewModel用來執行計數器的邏輯,dataState是一個StateFlow並且設定了初始值1,在onCallback裡面接收到了任務執行結果併發送至上層展示,上層的程式碼邏輯如下

image.png

現在這個計時器功能完成了,我們來執行下程式碼看看效果如何

aa3.gif

我們這邊使用StateFlow傳送資料還有個好處,當接收的資料中有些資料需要過濾掉的時候,我們還可以使用StateFlow提供的操作符實現,比如這邊我們只想展示奇數,那麼程式碼可以改成如下所示

image.png

使用filter操作符將偶數的值過濾掉了,我們再看看效果

aa4.gif

總結

我們的這個執行緒管理工具到這裡已經完成了,不是很複雜,但是專案當中存不存在這樣一個工具明顯會對整體開發效率,程式碼的可讀性,維護成本,以及一個app的效能角度來講都會有個很大的提升與改善,後面如果還做了其他優化工作,也會拿出來分享。