從零實現ImageLoader(三)—— 線程池詳解

目錄

從零實現ImageLoader(一)—— 架構
從零實現ImageLoader(二)—— 基本實現
從零實現ImageLoader(三)—— 線程池詳解
從零實現ImageLoader(四)—— Handler的內心獨白
從零實現ImageLoader(五)—— 內存緩存LruCache
從零實現ImageLoader(六)—— 磁盤緩存DiskLruCache

異步加載

既然是異步加載那新開線程自然是必不可少的。一個線程怎么樣?這種情況下圖片得一個一個依次加載,效率未免太低了。那每張圖片新開一個線程怎么樣?在圖片過多的情況下,線程數量也會迅速隨之增長,系統資源消耗太多嚴重,也不能接受。這時候就是線程池ExecutorService這個線程管理工具登場的時候了。

public class Dispatcher {
    private final String mUrl;
    private final ExecutorService mExecutorService;

    public Dispatcher(String url, ExecutorService executorService) {
        mUrl = url;
        mExecutorService = executorService;
    }

    public void into(ImageView imageView) {
        mExecutorService.execute(() -> {
            try {
                Bitmap image = get();
                //這一句將代碼切換到主線程,下一篇文章再詳細解釋
                ImageLoader.HANDLER.post(() -> imageView.setImageBitmap(image));
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    ...
}

ImageLoader類負責線程池的創建:

public class ImageLoader {
    ...
    
    private static final int MAX_THREAD_NUM = 3;
    private final ExecutorService mExecutorService;

    private ImageLoader(Context context) {
        //防止單例持有Activity的Context導致內存泄露
        mContext = context.getApplicationContext();
        mExecutorService = Executors.newFixedThreadPool(MAX_THREAD_NUM);
    }

    public Dispatcher load(String url) {
        return new Dispatcher(url, mExecutorService);
    }
}

這樣異步加載就實現完成了,測試一下:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageView imageView = findViewById(R.id.image);
        ImageLoader.with(this)
                .load("https://i.redd.it/20mplvimm8ez.jpg")
                .into(imageView);
    }
}
效果圖

線程池使用

當然我們今天的重點不在異步加載,而是在線程池上。

Executors

我們平時使用線程池只需要調用Executors.new**ThreadPool()方法,甚至都不需要關心創建的類是什么。那今天就從Executors入手去探尋線程池的廬山真面目:

public class Executors {
    ...

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
}

可以看到,平時使用最頻繁的這幾個方法基本都是直接創建了ThreadPoolExecutor類,只是參數有所不同。唯一比較特殊的ScheduledThreadPoolExecutor也繼承自ThreadPoolExecutor

ThreadPoolExecutor構造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize:線程池的核心線程數。當線程數小于corePoolSize時會創建一個線程去執行任務,當線程數達到corePoolSize時會將任務放入等待隊列。如果沒有手動調用核心線程超時,這些線程在創建后會一直存在。
  • maximumPoolSize:線程池允許創建的最大線程數。在等待隊列添加滿之后,線程池會創建臨時線程用來處理任務,臨時隊列在超時后會自動結束。而臨時線程與核心線程的總數不能超過maximumPoolSize
  • keepAliveTime:臨時線程超時時間。
  • unitkeepAliveTime時間單位。
  • workQueue:線程池的等待隊列。當線程數達到corePoolSize時會任務會被放入該等待隊列。
  • threadFactory:線程工廠。用于創建新線程。
  • handler:飽和處理策略。當線程池關閉或者線程數達到maximumPoolSize時,任務被放入該handler

在看完上面的一系列參數,可能還是一臉懵逼。其實大家可以線程池當做一個工廠,這個工廠負責對一些半成品進行加工,而核心線程就是這個工廠的工人

每個工人同時只能處理一個半成品,多余的半成品就被放入倉庫,等哪個工人處理完了手頭的半成品再來倉庫取,這個倉庫就是等待隊列

但是倉庫也有限制,隨著半成品越來越多倉庫也放不下了,這時候工廠就請來一些臨時工來幫忙,等工廠的任務輕了之后再請他們回去,這些臨時工就是臨時線程

可是工廠的資金也是有限的不能同時請太多的工人,這個資金限制就是maximumPoolSize,而這些既沒有工人處理,倉庫又放不下的半成品就要想個辦法處理了。是直接把它們丟掉?還是退回給半成品廠商?這就是飽和策略需要決定的了。

等待隊列

  • 同步移交隊列:任務不在隊列中存儲,而是直接交給工作線程。這時可以使用SynchronousQueue實現,該隊列保證在插入時必須有另一個線程在等待獲取,如果沒有則插入失敗。Executors.newCachedThreadPool()使用的就是該隊列。
  • 無界隊列:例如無界LinkedBlockingQueueExecutorService.newFixedThreadPoolExecutorService.newSingleThreadExecutor使用的就是該隊列。
  • 有界隊列:例如有界LinkedBlockingQueueArrayBlockingQueue。有界隊列避免了無界隊列無限制的增加導致資源耗盡的問題。

飽和策略

這里很明顯是策略模式,ThreadPoolExecutor給我們提供了四個已經實現好的飽和策略,不過我們也可以選擇自己實現:

  • AbortPolicy:拋出RejectedExecutionException
  • CallerRunsPolicy:將任務放到調用execute()所在線程執行,也就是直接調用任務的run()方法。
  • DiscardPolicy:直接丟棄任務,不做任何處理。
  • DiscardOldestPolicy:將等待隊列頭部的任務刪除,再重新執行此任務。

套路

說了這么多,那到底應該怎么選擇呢?其實只要大致遵循一個規律,如果是計算密集型的任務,線程池的大小設為CPU的數目加1通常是最優的,而如果是I/O密集型的任務就可以設置的大一些,比如2倍的CPU的數目。當然,具體的數目就要在運行過程中慢慢調試了。

線程池原理

講完了線程池的使用,接下來就是線程池的原理了。這次的分析都基于Android 7.1.1的源碼,其他版本的可能會在細節上有一些差異,不過大的方向不會有問題。

類結構

在了解ThreadPoolExecutor的實現之前我們首先對類的繼承結構要有一個整體的把握:

類結構

Executor是Java提供的用于簡化線程管理的接口,用戶只需通過execute()方法傳入Runnable的實現,由Executor決定使用哪個線程處理同時負責線程的創建、運行和關閉。

ExecutorService,這個是我們平時使用線程池最用的了,在Executor的基礎上又加入了submit()shutdown()等等一些方便用戶自主管理任務的方法。

AbstractExecutorService類實現了submit()等一系列方法,不過最主要的execute()依然留給了子類也就是今天的主角ThreadPoolExecutor去實現。

概覽

ThreadPoolExecutor實際上使用的是生產者/消費者模型,在分析具體的代碼之前我們先看一下這個流程圖,有一個大概的印象。

流程圖

execute()

關于execute()的處理過程,Java源碼有很詳細的注釋,這里我把它翻譯為中午供大家參考。

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * 三步走:
         *
         * 1. 如果當前運行的線程少于核心線程數,嘗試開啟一個線程并將command作為
         * 它的第一個任務(作者注:這里的第一個任務在后面會有所解釋)。調用
         * runWorker通過原子性的方式重新檢查線程池運行狀態和工作線程數,如果
         * 不能添加線程則返回false
         *
         * 2. 如果任務可以成功入列,我們依然要再次檢查線程池是否已經關閉以及
         * 是否需要添加一個新線程(因為存在一種情況是在上次檢查之后所有的線程都已經
         * 死光光了(作者注:至于為什么必須保證至少一個線程存活,我們在后面的
         * runWorker方法中會找到答案))。如果線程池已經關閉,則將之前加入
         * 隊列的command彈出;如果已經沒有線程存活,則添加一個新線程。
         *
         * 3. 如果不能將任務入列,我們會嘗試添加一個新線程。如果失敗了,要不
         * 就是線程池已經關閉了,要不就是線程已經飽和了(作者注:線程數達到最大值),
         * 這時候我們就將這個任務加入飽和策略。
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

這里有一點需要注意的是,這個ctl變量的含義。其實ctl就是一個保存了線程池運行狀態以及線程數的原子整形變量:

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

它的高3位存儲線程池運行狀態,即RUNNINGSHUTDOWNSTOPTIDYINGTERMINATED五個狀態,低位存儲運行的線程數,可以用isRunning()判斷線程池是否處于運行狀態,用workerCountOf獲取運行的線程數。這里的ctl的用法非常巧妙,強烈推薦大家去看一下源碼,這里由于篇幅所限就不再多說了。

addWorker()

    private boolean addWorker(Runnable firstTask, boolean core) {
        // 這一段for循環用來將ctl的值加1,如果線程池關閉或者線程數量
        // 達到限制,則直接返回false。
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 判斷線程是否關閉或者已經沒有需要執行的任務
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                // 根據core判斷當前線程數量是否已經達到限制
                // 如果core為true,則線程數不能大于核心線程數
                // 否則不能大于最大線程數
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // 將ctl的值加1,如果成功則跳出外循環
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                // 判斷再次期間線程池狀態是否已經發生改變,如果是則
                // 重新開始外循環,否則在內循環中再次嘗試對ctl值加1
                c = ctl.get(); 
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // 在得到主鎖mainLock之后再次檢查線程池的狀態
                    // 如果已經關閉則不再添加
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // 檢查線程t能否啟動
                            throw new IllegalThreadStateException();
                        // 將線程t加入線程集合
                        workers.add(w);
                        // 更新目前為止線程最多時達到的數目,與最大線程數
                        // 無關,調試用
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    // 正式開始運行線程t
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

addWorker()方法很有意思,它用一個core參數來區分是添加核心線程還是添加臨時線程,一個方法可以有不同的功能。這也是為什么上面的流程圖里,添加核心線程和臨時線程的箭頭上只有一個addWorker方法。

addWorker()添加線程的邏輯可以分為四步:

  1. 確保線程數不超過限制,并將ctl的計數加1。
  2. firstTask封裝為Worker
  3. Worker加入線程集合workers
  4. 啟動Worker的線程。

有人已經注意到addWorker()的參數名有點奇怪,明明只添加了一個任務為什么要叫firstTask呢?在addWorker()的代碼里firstTask傳入了Worker的構造器,后面一系列操作就都是相對Worker執行的,那Worker又對firstTask做了什么?

Worker

    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker. */
        public void run() {
            runWorker(this);
        }

        ...
    }

可以看到Worker也繼承了Runnable接口,在構造方法里Worker通過ThreadFactory新開了一個線程,而傳入的Runnable卻是自己,所以之前addWorker()里的代碼t.start()最終執行的將會是Workerrun()方法,也就是runWorker()

主循環runWorker

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

這里的邏輯初看起來可能是一頭霧水,其實很簡單。重點是這里的while循環,首先會判斷firstTask是否為空,如果不為空則分四步:

  1. 調用beforeExecute()方法。
  2. 調用taskrun()方法。
  3. 調用afterExecute()方法。
  4. task賦值為空。

下一次循環task必定為空,于是執行task = getTask(),這條語句是將Runnable任務從等待隊列workQueue里取出來賦值給task,于是再次執行上面四步,直到線程池關閉或者等待超時。

每個Worker創建的線程在執行完屬于自己的任務后,還會繼續執行等待隊列中的任務,所以這個firstTask也可以當做每個線程的啟動任務,這就是它為什么被叫做firstTask的原因,也是runWorker方法為什么被稱為主循環的原因,線程池的設計者巧妙的用這一方法實現了線程的復用。

這也解答了之前的許多疑問:

  • 為什么沒有專門處理的等待隊列的線程?原因就在于每個線程都是處理等待隊列的線程。
  • 為什么在execute()方法中將任務加入等待隊列時,必須保證至少有一個線程存活?這是為了確保存在存活線程去執行等待隊列中的任務。

總結

我們這次實現了圖片的異步加載,不過將重點放在了線程池的使用及其原理上,設計者的各種巧思也是讓我們嘆為觀止,大家如果有空可以自己嘗試看一下源碼,一定不會讓你們失望。下一篇文章我們將要講解的是Handler,敬請期待。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容