代碼優化一下,用線程池管理那些隨意創建出來的線程

語言: 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的性能角度來講都會有個很大的提升與改善,後面如果還做了其他優化工作,也會拿出來分享。