關於多線程下載功能,前四篇博文所講解內容已經實現,接下來需要對代碼進行優化。開發一個新功能並不複雜,難的是考慮到代碼的擴展性和解耦性,後續需要進行的bug修復、完善功能等方面。此篇內容主要講解代碼優化,將從線程優化、單例優化、設計優化這三個方面進行講解。
此篇內容將涉及到以下知識:
- 線程優化及Linux系統中線程調度介紹
- Android中常用的5種單例模式解析
- volatile關鍵字底層原理及注意事項
- 構建者模式介紹及使用
(建議閱讀此篇文章之前,需理解前兩篇文章的講解,此係列文章是環環相扣,不可缺一,鏈接如下:)
優雅設計封裝基於Okhttp3的網絡框架(一):Http網絡協議與Okhttp3解析
優雅設計封裝基於Okhttp3的網絡框架(二):多線程下載功能原理設計 及 簡單實現
優雅設計封裝基於Okhttp3的網絡框架(三):多線程下載功能核心實現 及 線程池、隊列機制解析
優雅設計封裝基於Okhttp3的網絡框架(四):多線程下載添加數據庫支持(greenDao)及 進度更新
一. 線程優化
1. 根本問題
在Java語言中本身存在線程的優先級,是否直接操作設置這些線程的優先級就可以控制Android程序中的線程?
並非如此,實際上Java提供的一些線程優先級設置對於Android而言並非起太大作用,因爲Android系統是基於Linux,而Linux系統對於線程調度管理有一套自己的法則。
2. Linux的線程調度法則
在Linux中使用nice value(以下成爲nice值)來設定一個進程的優先級,系統任務調度器根據nice值合理安排調度。在Android系統中,也是採用此值來進行優化,特點如下:
- nice的取值範圍爲-20到19。
- 通常情況下,nice的默認值爲0。
- nice的值越大,進程的優先級就越低,獲得CPU調用的機會越少,nice值越小,進程的優先級則越高,獲得CPU調用的機會越多。
- 一個nice值爲-20的進程優先級最高,nice值爲19的進程優先級最低。
以上便是 nice值的特點介紹,那麼如何在代碼中進行使用?首先來查看一個系統類AsyncTask,在它的內部實現中就使用到了相關代碼:
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
此行代碼作用相關於將該線程的優先級設置成THREAD_PRIORITY_BACKGROUND,繼續查看其優先級別:
查看源碼可知優先級別爲10,將它設置爲後臺線程的好處是(查看註釋):減少系統調度時間,UI線程會得到更多響應時間。
3. DownloadRunnable中的run方法優化
經過以上講解後,將設置線程優先級至後臺線程這行代碼添加至run
方法的第一行即可。(此行代碼設置雖簡單,但背後邏輯操作緊密聯繫Linux線程調度,有興趣者後續可多瞭解)
@Override
public void run() {
//設置線程優先級別爲後臺線程,爲了 減少系統調度時間,使UI線程會得到更多響應時間 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
......
}
二. 單例模式優化
此點將對於Android中使用的單例模式進行優化,單例模式使用的場景往往是:程序中的某些對象的創建和消耗是比較耗費資源的,此時可以考慮將其對象設置爲單例模式。
該模式算是設計模式中最常用、基本的一種,在目前已實現的網絡框架編碼中已多次使用。按照Java語言詳細劃分,單例模式可分爲7種,但在Android中常用的只有以下5種。
1. 五大種類
(1)餓漢式
【餓漢式】
public class DownloadManager {
private static DownloadManager sManager = new DownloadManager();
private DownloadManager() {
}
public static DownloadManager getInstance() {
return sManager;
}
實現方式
- 將該類的對象設置爲靜態成員變量;
- 類的構造方法設置爲私有,意味着外界無法創建該對象;
- 直接創建對象賦值給靜態成員變量。
創建時機
當此類加載進來的時候,該類的對象已經創建成功了。
(2)懶漢式
【懶漢式】
public class DownloadManager {
private static DownloadManager sManager;
private DownloadManager() {
}
public static DownloadManager getInstance() {
if (sManager == null) {
sManager = new DownloadManager();
}
}
return sManager;
}
實現方式
懶漢式的單例模式算是對餓漢式的優化:
- 將該類的對象設置爲靜態成員變量;
- 類的構造方法設置爲私有,意味着外界無法創建該對象;
- 在對外提供的public靜態
getInstance
方法進行對象創建操作。
創建時機
當需要該類的對象時,調用此類暴露出來的getInstance
方法,纔會去創建對象。
(3)Double Check
問題解析
單例模式的每一種方式衍生可以看作是一次次的完善。在懶漢式中的getInstance
方法創建類對象時,若遇到多線程的情況,便會出問題,即多個線程同時在操縱創建這行代碼,這樣對象並非是單例模式了。
改善代碼
最簡單的修改方法就是給getInstance
方法加上synchronized 關鍵字,鎖住此方法,但是鎖住這整個方法消耗頗多,並非最佳,實際上只需鎖住創建對象這行代碼即可。
於是,有了如下Double Check方式:
【Double Check】
public class DownloadManager {
private static DownloadManager sManager;
private DownloadManager() {
}
public static DownloadManager getInstance() {
if (sManager == null) {
synchronized (DownloadManager.class) {
if (sManager == null) {
sManager = new DownloadManager();
}
}
}
return sManager;
}
實現方式
- 將該類的對象設置爲靜態成員變量;
- 類的構造方法設置爲私有,意味着外界無法創建該對象
- 在對外提供的public靜態
getInstance
方法中判斷,當對象爲空時,使用synchronized 關鍵字,鎖住DownloadManager.class,在其代碼塊中再加一個判斷:若對象爲空時創建對象。
創建時機
當需要該類的對象時,調用此類暴露出來的getInstance
方法,纔會去創建對象。
優缺點
優點:只會在第一次創建對象的時候去加鎖,之後無需加鎖直接返回已存在對象,節省了不必要的性能消耗。
缺點:其實這種方法也不能保證程序的完整性,它在某些虛擬機上運行會出現空指針問題,背後原因比較複雜,簡單而言是因爲創建對象這行代碼並非原子性操作,虛擬機在編譯字節碼時將這行代碼分爲幾步操作。
舉個例子,當A線程訪問對象爲空,獲取到鎖,在其中進行對象創建過程中,線程B訪問該方法,判斷對象不爲空,獲取到此時返回的對象進行其它操作。注意此時線程B獲取的對象僅是不爲空,但它還未初始化完成,所以會出現空指針問題。原因就是字節碼在執行時並非是原子性操作。(這裏稍作了解即可,後續volatile介紹會繼續講解)
雖然空指針異常的發生機率不大,但畢竟是一個顯示問題,存在太大隱患,一旦發生異常,進行排除問題都難以想到此層面。此種方式不太推薦。
(4)靜態內部類
問題解析
面對最後 Double Check而言的問題,又有一種方式出現來解決此問題 —— 靜態內部類。它可以起到延遲加載的作用,並且能保證創建對象的完整性。
【靜態內部類】
public static class Holder {
private static DownloadManager sManager = new DownloadManager();
public static DownloadManager getInstance() {
return sManager;
}
}
//外界訪問
DownloadManager.Holder.getInstance();
實現方式
- 編寫訪問權限爲public 的靜態內部類;
- 在其內部類中將對象設置爲私有的靜態成員變量;
- 在其內部類中對外提供 public 獲取對象的靜態方法
getInstance
返回對象。
創建時機
調用該靜態內部類的getInstance
時纔會初始化對象。
原理分析
雖然它只是一個靜態內部類,但虛擬機在編譯class時會單獨將它放到一個文件。看起來跟餓漢式似乎有點相像,但是當虛擬機加載此類時,並不會初始化該對象,只有調用該靜態內部類的getInstance
時纔會初始化對象,起到了延遲加載作用,保證了創建對象的操作原子性。
此種方式較爲推薦。
(5)枚舉
其實枚舉實現單例模式這種方式在Java語言中較爲推崇,枚舉在虛擬機層面已經保證了創建的唯一性,但是在Android系統中並不推薦,因爲枚舉會導致佔用內存過多,可以嘗試反編譯枚舉的Class,會發現隨着枚舉定義類型的增多,它所佔用的內存是成倍增長,每一個枚舉類型都生成相應的Class,它的每一個成員變量都是單獨一份,所以此方式並不推薦!
2. volatile 關鍵字解析
在上一點中介紹 Double Check的單例模式使用中,會出現空指針問題,根本原因上述已簡單講解,此點將結合 volatile 關鍵字,從虛擬機底層原理詳細探究。
博主聲明: volatile 關鍵字向來是多線程併發中的重點,此點講解涉及到大量虛擬機底層相關原理知識,若想真正瞭解透徹,僅以此點遠遠不夠,推薦讀者能夠先查看以下文章鏈接,這是Java虛擬機中相關部分,學習之後再來理解Double Check單例模式中的空指針異常,會更加容易。
JVM高級特性與實踐(十二):高效併發時的內外存交互、三大特徵(原子、可見、有序性) 與 volatile型變量特殊規則
(1)異常定位—— 空指針異常
【Double Check】
public class DownloadManager {
private static DownloadManager sManager;
public static DownloadManager getInstance() {
if (sManager == null) {
synchronized (DownloadManager.class) {
if (sManager == null) {
//出錯處
sManager = new DownloadManager();
}
}
}
return sManager;
}
代碼異常定義
出現異常來源於創建對象那行代碼,看似只是一個對象創建並賦值操作,但是當虛擬機編譯成字節碼文件,這行代碼的對象創建操作不是一個原子性操作,會生成多條字節碼指令。
例子講解
再來回顧這個例子:線程A進入到getInstance()
方法時首先判斷對象爲空,拿到DownloadManager的鎖,準備對象創建操作。此時線程B進入該方法,該對象已經不爲空了(線程A已創建該對象實例),線程B理所當然獲取到對象,但是此時對象並不完整,它只是不爲空,初始化階段可能尚未完成,所以當線程B使用該對象操作時必然會出現空指針異常。
對象創建代碼 分解
那行代碼的對象創建操作不是一個原子性操作,通過僞碼的形式可分成以下幾步:
- 1)給 sManager 分配內存
- 2) sManager 調用構造方法進行初始化操作
- 3)對sManager 對象進行賦值操作,使它指向在第一步分配的內存區域
(2)根本原因——JVM中的字節碼指令集的重排序
所以說一個簡單的對象創建在Java虛擬機中會被劃分爲這3個步驟,其實這些步驟也很正常,需要注意的是Java虛擬機會對代碼步驟進行重排序,即字節碼指令集的重排序。
例如以上步驟二可能被虛擬機重排序到最後,這樣意味着步驟一結束後,對象確實不爲空了,但是在步驟三初始化之前被線程B獲取到對象實例,而導致空指針異常!
代碼執行順序
虛擬機執行代碼的順序並非是按照我們所寫的,而是以字節碼文件爲準。而JVM會自動優化代碼,打亂字節碼執行順序!注意:這裏的順序打亂它不會故意破壞而導致異常產生,例如代碼上下之間有依賴關係,JVM不會進行重排序。
(3)問題解決 —— volatile關鍵字
其實以上問題在單線程中並不會出現,只會在多線程中出現,爲了避免JVM中的字節碼指令集重排序問題,JDK 1.5中引入了一個關鍵字 —— volatile,它有兩個重要作用:
- 禁止JVM進行重排序
- 保證變量的可見性(可見性:指當一個線程修改了共享變量的值,其他能夠立即得知這個修改)
Java開發者應當知道當一個變量被volatile關鍵字修飾時,它擁有可見性,但是另外一個作用 —— 禁止JVM進行重排序,卻鮮爲人知。當次變量被修飾後,JVM不會打亂字節碼執行順序,而出現步驟二在最後執行的情況。
指令重排序例子再論
爲了更好的理解指令重排序,再舉個例子來了解,例如在以下代碼定義了這三個變量:
int a = 12;
boolean flag = false;
long c = 23;
JVM在執行以上代碼時會以字節碼文件爲準,即打亂順序,可能先操作boolean變量賦值,然後再是int,最後long。代碼原本順序的確被打亂了,但是JVM並不會無故打亂而導致異常產生,例如以下示例:
int a = 12;
int b = 10;
int c = a+23;
JVM在進行重排序時絕對不會將int c = a+23;
操作放到int a = 12;
之前,在程序編譯時JVM已經考慮到了這些變量之間的依賴(還有其它考慮原則),所以變量a的賦值一定在變量c之前完成,不過變量a、b的初始化順序無法保證。
(4)先行發生原則(happens-before)
JVM在處理相關的字節碼文件時,所考慮到的原則是規定好的,例如上述中變量之間的依賴,這些判斷的依據就是先行發生原則,由以下幾個規則組成:
- 程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在後面的操作,準確地說,應該是控制流順序。
- 管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作;這裏必須強調的是同一個鎖,而後面是指時間上的先後順序。
- volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,這裏的後面是指時間上的先後順序。
- 線程啓動規則(Thread Start Rule): Thread對象的start() 方法先行發生於此線程的每一個動作。
- 線程終止規則(Thread Temination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,可以通過Thread.join() 方法結束,Thread.isAlive() 的返回值等手段檢測到線程已經終止運行。
- 線程中斷規則(Thread Interruption Rule):對線程interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過 Thread.interrrupted() 方法檢測到是否有中斷髮生。
- 對象終結規則(Finalizer Rule):一個對象的初始化完成先行發生於它的finalize() 方法的開始。
- 傳遞性(Transitivity):如果操作A 先行發生於操作B, 操作B 先行發生於操作C,那就可以得出操作A 先行發生於 操作C的結論。
如果編寫的代碼中滿足以上規則,JVM不會對字節碼指令集進行優化,即重排序。
最後,在《深入Java虛擬機》中從底層原理部分詳細解析了volatile關鍵字,若要透徹瞭解,可查看此書(12章的3.3節)或博主寫的記錄博客。
三.設計優化——構建者(Build)模式
若讀者對構建者模式的組成及使用不熟悉,建議先看以下博文,以下博文詳細講解了構建者模式的構造及使用,舉例說明學習。
目前有個需求,在DownloadManager管理類中,想要提供一些靈活的參數來控制此類中的線程池、執行服務對象創建,例如核心、最大線程數這些參數等。如此而言就需要在此類中設計多個set
方法,而不同參數的組合可能導致set
方法需求量的增加,代碼冗雜,所以採用構建者模式來解決這種需求帶來的問題。
1. 構建者(Build)模式
(1)定義及作用
定義:將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。
作用:減少對象創建過程中引入的多個重載構造函數、可選參數以及setter
過度使用導致不必要的複雜性。
(2)組成
一般而言,Builder模式主要由四個部分組成:
- Product :被構造的複雜對象,ConcreteBuilder 用來創建該對象的內部表示,並定義它的裝配過程。
- Builder :抽象接口,用來定義創建 Product 對象的各個組成部分的組件。
- ConcreteBuilder : Builder接口的具體實現,可以定義多個,是實際構建Product 對象的地方,同時會提供一個返回 Product 的接口。
- Director : Builder接口的構造者和使用者。
(3)實例講解
這裏舉的例子應該算是構建者模式的進化版,精簡了一些不必要的接口,若想要了解標準的模式編碼,可以看第三點開頭給出的鏈接,在此不多言。
public class User {
private final String mName; //必選
private final String mGender; //可選
private final int mAge; //可選
private final String mPhone; //可選
public User(UserBuilder userBuilder) {
this.mName = userBuilder.name;
this.mGender = userBuilder.gender;
this.mAge = userBuilder.age;
this.mPhone = userBuilder.phone;
}
public String getName() {
return mName;
}
public String getGender() {
return mGender;
}
public int getAge() {
return mAge;
}
public String getPhone() {
return mPhone;
}
public static class UserBuilder{
private final String name;
private String gender;
private int age;
private String phone;
public UserBuilder(String name) {
this.name = name;
}
public UserBuilder gender(String gender){
this.gender = gender;
return this;
}
public UserBuilder age(int age){
this.age = age;
return this;
}
public UserBuilder phone(String phone){
this.phone = phone;
return this;
}
public User build(){
return new User(this);
}
}
}
從以上代碼可以看出這幾點:
- User類的構造函數是私有的,這意味着調用者不可直接實例化這個類。
- User類是不可變的,其中必選的屬性值都是 final 的並且在構造函數中設置;同時對所有的屬性取消 setters函數,只保留 getter函數。
- UserBuilder 的構造函數只接收必選的屬性值作爲參數,並且只是將必選的屬性設置爲 fianl,來保證它們在構造函數中設置。
接下來,User類的使用方法如下:
public User getUser(){
return new
User.UserBuilder("gym")
.gender("female")
.age(20)
.phone("12345678900")
.build();
}
以上通過 進化的Builder模式形象的體現在User的實例化。
2. DownloadConfig(採用構建者模式)
創建一個配置類採用構建者模式對外提供參數配置:
public class DownloadConfig {
private int coreThreadSize;
private int maxThreadSize;
private int localProgressThreadSize;
private DownloadConfig(Builder builder) {
coreThreadSize = builder.coreThreadSize == 0 ? DownloadManager.MAX_THREAD : builder.coreThreadSize;
maxThreadSize = builder.maxThreadSize == 0 ? DownloadManager.MAX_THREAD : builder.coreThreadSize;
localProgressThreadSize = builder.localProgressThreadSize == 0 ? DownloadManager.LOCAL_PROGRESS_SIZE : builder.localProgressThreadSize;
}
public int getCoreThreadSize() {
return coreThreadSize;
}
public int getMaxThreadSize() {
return maxThreadSize;
}
public int getLocalProgressThreadSize() {
return localProgressThreadSize;
}
public static class Builder {
private int coreThreadSize;
private int maxThreadSize;
private int localProgressThreadSize;
public Builder setCoreThreadSize(int coreThreadSize) {
this.coreThreadSize = coreThreadSize;
return this;
}
public Builder setMaxThreadSize(int maxThreadSize) {
this.maxThreadSize = maxThreadSize;
return this;
}
public Builder setLocalProgressThreadSize(int localProgressThreadSize) {
this.localProgressThreadSize = localProgressThreadSize;
return this;
}
public DownloadConfig builder() {
return new DownloadConfig(this);
}
}
}
3. 代碼整合
(1)DownloadManager
在完成採用構建者模式的配置類,那麼DownloadManager類中線程池的創建可直接調用配置類,需要在DownloadManager類中稍作修改。
private static ExecutorService sLocalProgressPool;
private static ThreadPoolExecutor sThreadPool;
public void init(DownloadConfig config) {
sThreadPool = new ThreadPoolExecutor(config.getCoreThreadSize(), config.getMaxThreadSize(), 60, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(), new ThreadFactory() {
private AtomicInteger mInteger = new AtomicInteger(1);
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "download thread #" + mInteger.getAndIncrement());
return thread;
}
});
sLocalProgressPool = Executors.newFixedThreadPool(config.getLocalProgressThreadSize());
}
(2)Application初始化
DownloadConfig config = new DownloadConfig.Builder()
.setCoreThreadSize(2)
.setMaxThreadSize(4)
.setLocalProgressThreadSize(1)
.builder();
DownloadManager.getInstance().init(config);
以上,這種動態的設置配置參數處理方式相較於構造方法,靈活得多,減少了大量不必要的冗雜代碼,擴展性較強,可鏈式增添參數配置。
四. 總結
1. 本篇總結
本篇內容是對前四篇博文完成的編碼工作進行的優化,需要修改的地方並不多,但是爲了程序的擴展性、解耦性、線程安全性考慮,分別從線程優化、單例優化、設計優化這三個層面對代碼進行優化。其實完成一個功能並不難,重要的是前期設計部分一定要思考清楚功能可行性、程序擴展性等問題,而優化工作更是必不可少,切勿全部編程完再來一次“重構”,這樣開發週期會被無限拖長,代碼質量並不會因爲所謂的“重構”而提高,適時的時候停下來對已完成的功能進行優化。
EasyOkhttp網絡框架封裝源碼(對應第五篇博文優化後地代碼)
2. 下篇預告
到目前爲止,多線程下載功能設計、編寫、優化工作已經完成,但是網絡框架功能並沒有完成,下篇將編寫的新功能還是圍繞在http請求上:
- httpHeader的接口定義和實現
- http請求頭和響應頭訪問編寫
- http狀態碼定義
- http中的 response封裝、request接口封裝和實現
若有錯誤,虛心指教~