優雅設計封裝基於Okhttp3的網絡框架(五):多線程、單例模式優化 及 volatile、構建者模式使用解析

關於多線程下載功能,前四篇博文所講解內容已經實現,接下來需要對代碼進行優化。開發一個新功能並不複雜,難的是考慮到代碼的擴展性和解耦性,後續需要進行的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)模式

若讀者對構建者模式的組成及使用不熟悉,建議先看以下博文,以下博文詳細講解了構建者模式的構造及使用,舉例說明學習。

Android : Builder模式 詳解及學習使用

目前有個需求,在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接口封裝和實現


若有錯誤,虛心指教~

發佈了115 篇原創文章 · 獲贊 256 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章