天天看點

android中線程的總結前言AsyncTaskHandlerThreadIntentServiceThreadPoolExecutor總結參考文獻

android

thread

線程

前言

開發安卓幾年了,由于工作比較忙平常也沒太整理基礎知識,現在覺得基礎知識需要系統的複習。今天就把安卓的線程複習一下。 

安卓常用到的線程有哪些呢?

  • Thread:基本線程,沒啥說的
  • AsyncTask:這個類可以說是Handler+Thread的組合。我們平常用Handler可以解決些簡單的線程間通信問題,但是當遇到複雜點需求時,用Handler寫代碼就太臃腫了,AsyncTask幫我們做了封裝,讓代碼寫起來更優雅,使用更友善。
  • HandlerThread:這個類其實就是一個線程,它繼承了Thread,但是在它的方法裡,建立了一個looper,就是說在HandlerThread線程裡建立了一個消息隊列來處理事情。
  • IntentService:這個類繼承了Service,是以它是一個服務,但它裡邊實作用的是HandlerThread,那既然有了HandlerThread為啥還要出來個IntentService啊,因為Thread的優先級是比Service的優先級低的,就是說我們建立一個HandlerThread線程比IntentService線程更容易被系統kill掉。
  • java工具類線程池:ThreadPoolExecutor,這個就是java為我們實作的線程池類。

AsyncTask

  • 雖然說它是Handler+Thread的組合,但是并不是簡單的Thread,而是線程池。
  • 因為有Handler的存在,它跟hanler一樣必須在主線程建立,雖然是線程池的構造,但是它預設是串行執行任務的,而不是并發執行。為什麼這樣設計呢,因為設計者考慮到我們這些菜鳥在調用AsyncTask時候,這塊調用一次,那塊調用一次,可能不小心會兩個task都調用這塊共享資源,這就可能造成并發問題,而我們這些菜鳥并不太了解Asynctask,是以沒有處理并發的意識,造成程式由于并發而産生意外的結果。是以預設是串行執行任務的。當然如果你不是菜鳥可以修改它的預設設定,改成并發執行。
  • 由于串行執行任務的設計,假如一個task執行一個任務耗時超過5秒,後邊任務還處于等待,是以使用AsyncTask執行的任務,盡量不要超過5秒(官方建議)。
  • 因為AsyncTask跟Handler類似,是以也會出現記憶體洩露問題,我們需要使用static+Weakreference方案來防止記憶體洩露。

HandlerThread

HandlerThread其實本身就是一個Thread,跟普通Thread的不同之處在于,它内部維護了一個loop隊列。

對于這種設計我們在日常中有什麼應用呢?

就是一些頻率高的耗時處理,用HandlerThread做再合适不過了。

舉個例子,比如人臉識别操作,相機一直在高效的采集圖像,每秒鐘需要處理10張圖檔做對比,我們隻需要把最後的結果交給UI線程更新就行了,至于高頻的對比操作過程,交給HandlerThread來做。這樣可以避免主線程的卡頓。

當然我們還是要注意,HandlerThread的線程Priority不是太高,執行耗時操作容易被kill,我們可以在構造方法裡設定它的優先級,這樣結合自己的實際業務來設定對應的級别,畢竟虛拟機隻允許少量的進階别線程工作。

IntentService

大家都清楚,在Android的開發中,凡是遇到耗時的操作盡可能的會交給Service去做,比如我們上傳多張圖,上傳的過程使用者可能将應用置于背景,然後幹别的去了,我們的Activity就很可能會被殺死,是以可以考慮将上傳操作交給Service去做,如果擔心Service被殺,還能通過設定

startForeground(int, Notification)

方法提升其優先級。

那麼,在Service裡面我們肯定不能直接進行耗時操作,一般都需要去開啟子線程去做一些事情,自己去管理Service的生命周期以及子線程并非是個優雅的做法;好在Android給我們提供了一個類,叫做

IntentService

上邊是引用鴻洋大神部落格IntentService的開篇介紹詳細。

說的很清楚,也是我上邊提到的,為了提升優先級防止被kill,我們将HandlerThread提升到IntentService。

ThreadPoolExecutor

當需要處理耗時操作時,我們希望線程并行處理來提高效率,用線程池來處理是在合适不過了。

其實這塊我們應該注意一點,何時使用多線程,我覺得下邊這個例子說的很恰當:

舉個例子,你要做飯,你要做的飯是米飯和一個炒菜。

如果是單線程,那麼你可以如下做:

第一種方法:先炒菜,然後開始蒸米飯;

第二種方法:先蒸米飯,等米飯熟了再炒菜;

如果是多線程,那麼你就可以如下做:

先蒸米飯,在蒸米飯的過程中去炒菜。

有些問題的解決用多線程會提高效率,比如上邊的例子。但是有時不會提高效率,反而會影響效率:

比如,你要洗衣服,還打算做家庭作業(假設你是國小生,老師給你布置的家庭作業)。

如果是單線程:你要麼洗完衣服做作業,要麼做完作業洗衣服。

如果是多線程:你洗一分鐘衣服做一分鐘作業,交叉進行,顯然有些時間都耗在了任務的切換上了。

是以,多線程主要用于,當一個任務需要不占用資源的等待的時候,可以使用空閑的資源做其他的事情。比如類似于QQ聊天的程式,程式的一個線程一直在等待着看是否有好友發消息過來,而與此同時另一個線程允許你打字并且将自己的消息發送給對方。

以上例子并不是很完美,隻是希望能借這些例子對多線程有所了解。

就是說當有2個或2個以上任務的時候,每個任務中間都有等待時間,這個等待時間如果用單線程,是完全的浪費資源,如果用多線程,能充分的利用等待時間來做其他事情。在我們的日常工作中,有這種等待時間的操作,一般是IO操作,網絡,圖檔處理等。是以這方面需要用到多線程,來彌補等待的時間。

何時用線程池呢?我覺得是任務比較多的情況下,用線程池來處理再合适不過了。舉個例子:

當你現有有個需求,需要處理100個任務。考慮到機器的效能,我們隻能最多讓5個線程同時執行任務。那如何讓5個線程來執行100個任務呢?因為每個線程隻能對應一個任務來執行,是以執行的邏輯保證是先配置設定5個線程5個任務,然後就是哪個線程先執行完了,接着執行下一個任務。如果讓我們來配置設定這個任務。是不是感覺有點麻煩?如果利用線程池,這些任務我們就不用操心了。直接建立newFixedThreadPool(5),周遊100個任務,執行execute方法100次即可。省去了我們自己去維護5個線程,給5個線程配置設定任務的代碼。

說道java的線程池有些類我們總是模糊,Executor,Executors,ThreadPoolExecutor,ExecutorService,方法newFixedThreadPool,newFixedThreadPool,newSingleThreadExecutor等。 

  • Executor是線程池的最頂端,ThreadPoolExecutor和ExecutorService就是實作的這個接口。接口裡就一個方法execute。 
  • Executors相當于一個工具類,它的裡邊都是靜态方法,像newFixedThreadPool,newFixedThreadPool,newSingleThreadExecutor都是這個類的靜态方法。 
  • ThreadPoolExecutor是java實作的線程池類,像newFixedThreadPool,newFixedThreadPool,newSingleThreadExecutor這些方法其實都是new的ThreadPoolExecutor執行個體,隻是傳的參數不一樣而已。

是以說,在上邊的簡單介紹可以知道,我們主要了解ThreadPoolExecutor就行了,說到底都是new的這個類對象。

合理的使用線程池能夠帶來3個很明顯的好處:(參考)

1.降低資源消耗:通過重用已經建立的線程來降低線程建立和銷毀的消耗

2.提高響應速度:任務到達時不需要等待線程建立就可以立即執行。

3.提高線程的可管理性:線程池可以統一管理、配置設定、調優和監控。

構造方法的參數,我們還是有必要了解一下的(參考):

  • corePoolSize

    核心線程數,預設情況下核心線程會一直存活,即使處于閑置狀态也不會受存keepAliveTime限制。除非将allowCoreThreadTimeOut設定為true。

  • maximumPoolSize

    線程池所能容納的最大線程數。超過這個數的線程将被阻塞。當任務隊列為沒有設定大小的LinkedBlockingDeque時,這個值無效。

  • keepAliveTime

    非核心線程的閑置逾時時間,超過這個時間就會被回收。

  • unit

    指定keepAliveTime的機關,如TimeUnit.SECONDS。當将allowCoreThreadTimeOut設定為true時對corePoolSize生效。

  • workQueue

    線程池中的任務隊列BlockingQueue.常用的有三種隊,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue;

  • threadFactory

    線程工廠,提供建立新線程的功能。ThreadFactory是一個接口,隻有一個方法

    通過線程工廠可以對線程的一些屬性進行定制。

    如果不傳入這個值,系統會預設使用Executors.defaultThreadFactory()。

  • RejectedExecutionHandler

    RejectedExecutionHandler也是一個接口,隻有一個方法

    void rejectedExecution(Runnable var1, ThreadPoolExecutor var2);

    當線程池中的資源已經全部使用,添加新線程被拒絕時,會調用RejectedExecutionHandler的rejectedExecution方法。

線程池中的幾個狀态還是需要知道的:(參考)

  1. RUNNING:可以新增任務, 同時可以處理隊列中的任務;
  2. SHUTDOWN:不能新增任務,但是可以處理隊列中的任務;狀态切換:由RUNNING到SHUTDOWN通過shutdown方法來切換。
  3. STOP:不接收新任務,不處理已添加的任務,并且會中斷正在處理的任務。狀态切換:由(RUNNING或者SHUTDOWN)到STOP通過shutdownnow方法來切換。
  4. TIDIING:當所有的任務已終止,ctl記錄的”任務數量”為0,線程池會變為TIDYING狀态。當線程池變為TIDYING狀态時,會執行鈎子函數terminated()。terminated()在ThreadPoolExecutor類中是空的,若使用者想線上程池變為TIDYING時,進行相應的處理;可以通過重載terminated()函數來實作。狀态切換:當線程池在SHUTDOWN狀态下,阻塞隊列為空并且線程池中執行的任務也為空時,就會由 SHUTDOWN 到TIDYING。當線程池在STOP狀态下,線程池中執行的任務為空時,就會由STOP -> TIDYING。
  5. TERMINATED:線程池徹底終止,就變成TERMINATED狀态。狀态切換:線程池處在TIDYING狀态時,執行完terminated()之後,就會由 TIDYING -> TERMINATED。

分析ThreadPoolExecutor類我們可以從execute方法開始分析。我将execute方法做了注釋,如下:

/**
     * Executes the given task sometime in the future.  The task
     * may execute in a new thread or in an existing pooled thread.
     *
     * If the task cannot be submitted for execution, either because this
     * executor has been shutdown or because its capacity has been reached,
     * the task is handled by the current {@code RejectedExecutionHandler}.
     *
     * @param command the task to execute
     * @throws RejectedExecutionException at discretion of
     *         {@code RejectedExecutionHandler}, if the task
     *         cannot be accepted for execution
     * @throws NullPointerException if {@code command} is null

	 翻譯:在将來的某個時刻執行所給的任務,任務可能在新建立的線程裡執行也可能
	 在一個已經存線上程池中的線程執行,如果一個任務由于一些原因不能送出到線程池,
	 或者因為executor已經關閉了,又或者線程池的容量已經滿了,任務就會被構造方法
	 裡傳入的RejectedExecutionHandler對象拒絕。
     */
	 
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.

        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);
    }
           

這個流程基本能簡單的明白線程池的建立線程原理,可以概括為:

1.如果目前工作線程數,少于核心線程數,對于新添加進來的任務,會通過建立線程來執行這個任務。

2.如果目前工作線程數大于等于核心線程數,則會将任務添加到隊列中,如果向任務隊列添加不成功的話,跳入下個判斷。

3.如果向任務隊列添加任務不成功的話,則嘗試建立線程來執行任務,如果建立線程不成功的,抛出異常。

當然還有細節需要分析,比如addWorker裡的源碼,這裡就不展開寫了。

Executors中的幾個線程池:

FixedThreadPool 構造方法傳入一個int值,該值代表核心線程數和工作線程數一樣,非核心線程消亡時間是0秒,使用的隊列是linkedBlockingQueue.這個線程池的特點是,你int值傳入是多少,就會建立多少個線程,然後工作的話,就是這幾個線程來按照任務的先後順序依次執行。

SingledThreadPool,這個就是上邊線程的特殊情況,相當于int值是1的情況。這種使用場景是當我們需要一個線程,來處理多個任務時,這些任務會按照隊列的形式排好順序。等待線程執行。

CacheThreaPool,這個線程池的特點是多少個任務就會配置設定多少個線程。線程和任務是一一對應的關系。這個要主要,任務太多的話,系統會崩潰。使用場景就是任務數量有限,而且需要每個任務對應不同線程的情況。

DelayThreaPool,延時線程,就是按照固定時間間隔來執行任務

在ThreadPoolExecutor的構造方法裡我們看到有傳入隊列BlockingQueue(阻塞隊列),阻塞隊列也是一個很重要的概念。

BlockingQueue是一個接口,裡邊定義了許多方法,下邊列一些核心方法:

放入資料:

  offer(anObject):表示如果可能的話,将anObject加到BlockingQueue裡,即如果BlockingQueue可以容納,

    則傳回true,否則傳回false.(本方法不阻塞目前執行方法的線程)

  offer(E o, long timeout, TimeUnit unit),可以設定等待的時間,如果在指定的時間内,還不能往隊列中

    加入BlockingQueue,則傳回失敗。

  put(anObject):把anObject加到BlockingQueue裡,如果BlockQueue沒有空間,則調用此方法的線程被阻斷

    直到BlockingQueue裡面有空間再繼續.

擷取資料:

  poll(time):取走BlockingQueue裡排在首位的對象,若不能立即取出,則可以等time參數規定的時間,

    取不到時傳回null;

  poll(long timeout, TimeUnit unit):從BlockingQueue取出一個隊首的對象,如果在指定時間内,

    隊列一旦有資料可取,則立即傳回隊列中的資料。否則知道時間逾時還沒有資料可取,傳回失敗。

  take():取走BlockingQueue裡排在首位的對象,若BlockingQueue為空,阻斷進入等待狀态直到

    BlockingQueue有新的資料被加入; 

  drainTo():一次性從BlockingQueue擷取所有可用的資料對象(還可以指定擷取資料的個數), 

    通過該方法,可以提升擷取資料效率;不需要多次分批加鎖或釋放鎖。

java.util.current包裡為我們準備了幾種阻塞隊列都是實作的BlockingQueue,以下内容參考

1. ArrayBlockingQueue

      基于數組的阻塞隊列實作,在ArrayBlockingQueue内部,維護了一個定長數組,以便緩存隊列中的資料對象,這是一個常用的阻塞隊列,除了一個定長數組外,ArrayBlockingQueue内部還儲存着兩個整形變量,分别辨別着隊列的頭部和尾部在數組中的位置。

  ArrayBlockingQueue在生産者放入資料和消費者擷取資料,都是共用同一個鎖對象,由此也意味着兩者無法真正并行運作,這點尤其不同于LinkedBlockingQueue;按照實作原理來分析,ArrayBlockingQueue完全可以采用分離鎖,進而實作生産者和消費者操作的完全并行運作。Doug Lea之是以沒這樣去做,也許是因為ArrayBlockingQueue的資料寫入和擷取操作已經足夠輕巧,以至于引入獨立的鎖機制,除了給代碼帶來額外的複雜性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue間還有一個明顯的不同之處在于,前者在插入或删除元素時不會産生或銷毀任何額外的對象執行個體,而後者則會生成一個額外的Node對象。這在長時間内需要高效并發地處理大批量資料的系統中,其對于GC的影響還是存在一定的差別。而在建立ArrayBlockingQueue時,我們還可以控制對象的内部鎖是否采用公平鎖,預設采用非公平鎖。

2. LinkedBlockingQueue

      基于連結清單的阻塞隊列,同ArrayListBlockingQueue類似,其内部也維持着一個資料緩沖隊列(該隊列由一個連結清單構成),當生産者往隊列中放入一個資料時,隊列會從生産者手中擷取資料,并緩存在隊列内部,而生産者立即傳回;隻有當隊列緩沖區達到最大值緩存容量時(LinkedBlockingQueue可以通過構造函數指定該值),才會阻塞生産者隊列,直到消費者從隊列中消費掉一份資料,生産者線程會被喚醒,反之對于消費者這端的處理也基于同樣的原理。而LinkedBlockingQueue之是以能夠高效的處理并發資料,還因為其對于生産者端和消費者端分别采用了獨立的鎖來控制資料同步,這也意味着在高并發的情況下生産者和消費者可以并行地操作隊列中的資料,以此來提高整個隊列的并發性能。

作為開發者,我們需要注意的是,如果構造一個LinkedBlockingQueue對象,而沒有指定其容量大小,LinkedBlockingQueue會預設一個類似無限大小的容量(Integer.MAX_VALUE),這樣的話,如果生産者的速度一旦大于消費者的速度,也許還沒有等到隊列滿阻塞産生,系統記憶體就有可能已被消耗殆盡了。

ArrayBlockingQueue和LinkedBlockingQueue是兩個最普通也是最常用的阻塞隊列,一般情況下,在處理多線程間的生産者消費者問題,使用這兩個類足以。

3. DelayQueue

      DelayQueue中的元素隻有當其指定的延遲時間到了,才能夠從隊列中擷取到該元素。DelayQueue是一個沒有大小限制的隊列,是以往隊列中插入資料的操作(生産者)永遠不會被阻塞,而隻有擷取資料的操作(消費者)才會被阻塞。

使用場景:

  DelayQueue使用場景較少,但都相當巧妙,常見的例子比如使用一個DelayQueue來管理一個逾時未響應的連接配接隊列。

4. PriorityBlockingQueue

      基于優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定),但需要注意的是PriorityBlockingQueue并不會阻塞資料生産者,而隻會在沒有可消費的資料時,阻塞資料的消費者。是以使用的時候要特别注意,生産者生産資料的速度絕對不能快于消費者消費資料的速度,否則時間一長,會最終耗盡所有的可用堆記憶體空間。在實作PriorityBlockingQueue時,内部控制線程同步的鎖采用的是公平鎖。

5. SynchronousQueue

      一種無緩沖的等待隊列,類似于無中介的直接交易,有點像原始社會中的生産者和消費者,生産者拿着産品去集市銷售給産品的最終消費者,而消費者必須親自去集市找到所要商品的直接生産者,如果一方沒有找到合适的目标,那麼對不起,大家都在集市等待。相對于有緩沖的BlockingQueue來說,少了一個中間經銷商的環節(緩沖區),如果有經銷商,生産者直接把産品批發給經銷商,而無需在意經銷商最終會将這些産品賣給那些消費者,由于經銷商可以庫存一部分商品,是以相對于直接交易模式,總體來說采用中間經銷商的模式會吞吐量高一些(可以批量買賣);但另一方面,又因為經銷商的引入,使得産品從生産者到消費者中間增加了額外的交易環節,單個産品的及時響應性能可能會降低。

  聲明一個SynchronousQueue有兩種不同的方式,它們之間有着不太一樣的行為。公平模式和非公平模式的差別:

  如果采用公平模式:SynchronousQueue會采用公平鎖,并配合一個FIFO隊列來阻塞多餘的生産者和消費者,進而體系整體的公平政策;

  但如果是非公平模式(SynchronousQueue預設):SynchronousQueue采用非公平鎖,同時配合一個LIFO隊列來管理多餘的生産者和消費者,而後一種模式,如果生産者和消費者的處理速度有差距,則很容易出現饑渴的情況,即可能有某些生産者或者是消費者的資料永遠都得不到處理。

當我們了解了這幾個隊列後,我們看下在Executors方法裡都用的哪個隊列。

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

我們常用的newFixedThread方法用到的隊列是LinkedBlockingQueue.

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

我們常用的newCachedThreadPool方法用到的隊列是SynchronousQueue。

總結

以上呢,隻是這幾個線程的思想概述,具體的用法,還有一些細節的東西,我們還是要深入了解和學習的。針對每一個類的詳細介紹這塊就不寫了,網上有大把現成的文章。

參考文獻

Developers 

java多線程以及Android多線程 

你真的了解AsyncTask