線程池與Android的日日夜夜

線程池與Android的日日夜夜

假如你Java中研究到了線程池的話,一般來說,你已經對線程的原理頗有研究了,或者說,你意識到了線程的某些瓶頸或者缺點。你說,要有光,所以,天降線程池。

1.jpg

正兒八經的說,如果你爲每一個請求創建一個新的線程,這在性能上影響是巨大的,因爲線程對象的創建銷燬需要Java虛擬機頻繁的GC,假如說,一個請求所用的時間比創建銷燬線程對象時間還短的話,那麼時間將會大程度浪費在虛擬機的GC上,系統性能降低。

2.jpg

所以啊,線程池主要就是複用線程對象,就跟上面所說,解決線程對象頻繁創建和銷燬的問題,內部可以抽象成一個“池”,線程對象放在裏頭,需要用的時候就拿出來用,不用了就泡着,泡壞了或者不要了就清掉。也正因爲如此,線程池可以用來處理高併發的訪問請求

目錄

  1. 先從最基本的線程的3種用法說起
  2. 一個最基本的線程池用例
  3. 分析各種參數:線程池創建的ThreadPoolExecutor類
  4. 常見阻塞隊列及使用場景
  5. 比較Executors中3種線程池的區別和使用情景
  6. 對比線程和線程池的優缺點,各種使用場景及其區別
  7. 其他:併發集合框架
  8. 默認Executors生成線程池和自傳參數進構造方法ThreadPoolExecutor創建線程池的利弊
  9. 分析實際應用,如OkHttp中的線程池,如AsyncTask中的線程池,RxJava中的線程池

一 先從最基本的線程開始

先重新瞭解一下,創建線程的三種方法:
  1. 繼承Thread類創建線程
  2. 實現Runnable創建線程
  3. 實現Callable接口 、使用Future類接收返回值

(1)繼承Thread類,重寫父類run方法

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("biubiubiu");
    }

    public static void main(String[] arg){
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

(2)實現Runnable接口,實現接口的run方法

public class MyRunnable implements Runnable {

    public static void main(String[] arg) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("光頭強和熊大熊二");
    }
}

當然,我們最常用的是匿名的內部Runnable類

public class MyRunnable {

    public static void main(String[] arg) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("光頭強的斧頭");
            }
        });
        thread.start();
    }
}

(3)實現Callable接口,使用Future來接收返回值(接收可選)

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<String> {

    public static void main(String[] arg) {

        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        new Thread(futureTask).start();
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String call() throws Exception {


        return "豬豬俠";
    }
}

二、一個最基本的線程池用例

先放一個基本的線程池,這裏構造的是核心線程爲2,最大線程數爲5,有界阻塞數列爲5的線程池

import java.util.concurrent.*;

public class MyDemo {

    public static void main(String[] arg) {
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 60, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));

        for (int i = 0; i < 13; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("當前順序是:" + finalI + ",線程名字" + Thread.currentThread().getName());
                }
            });
        }
    }
}

console如下

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task MyDemo$1@135fbaa4 rejected from java.util.concurrent.ThreadPoolExecutor@45ee12a7[Running, pool size = 5, active threads = 5, queued tasks = 5, completed tasks = 0]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at MyDemo.main(MyDemo.java:10)
當前順序是:0,線程名字pool-1-thread-1
當前順序是:2,線程名字pool-1-thread-1
當前順序是:3,線程名字pool-1-thread-1
當前順序是:4,線程名字pool-1-thread-1
當前順序是:5,線程名字pool-1-thread-1
當前順序是:6,線程名字pool-1-thread-1
當前順序是:1,線程名字pool-1-thread-2
當前順序是:7,線程名字pool-1-thread-3
當前順序是:8,線程名字pool-1-thread-4
當前順序是:9,線程名字pool-1-thread-5

先看這裏的console,這裏打印了RejectedExecutionException異常,還有打印了10個線程執行方法體裏頭的信息。後面的線程名字有6個1。其他的都是單獨的2,3,4,5號,這裏說明線程在線程池中得到了複用。

三、線程池創建的ThreadPoolExecutor類

ThreadPoolExecutor類有4個構造方法,其中的三個構造方法最終會調用參數最多的(7個)的構造方法

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {

    ..

構造方法各個參數:
1. corePoolSize:線程池中的核心線程數,一般情況設置爲CPU核心數
2. maximumPoolSize:線程池的線程數量最大值,非核心線程數=最大值-核心線程數
3. keepAliveTime:非核心線程閒置時候的超時回收時間,要是想多任務(該任務輕量執行內容/塊)下線程的利用率,可以增大這個超時時間
4. unit:上面這個參數的單位,有分秒毫秒等等
5. workQueue:線程池的任務隊列。新建的線程數超過核心線程時,線程加入任務隊列進 行等待或者分發。常用的有ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue
6. threadFactory:線程池的線程工廠。常用來設置每個線程的名字。默認名字是 pool-1-thread-1,一般默認爲Executors.defaultThreadFactory()即可,當然,ThreadPoolExecutor類的構造方法最終都是傳入DefaultThreadFactory
7. RejectedExecutionHandler:飽和策略。當任務隊列和線程池都達到最大值時的處理策略。默認是無法處理新的任務的AbortPolicy。比如上面第二節console輸出的是AbortPolicy策略。那是因爲創建的最大線程數是5,任務隊列是5,那麼線程池中會存在10個線程,而我創建了13個線程的同時超過了10個線程,接着就會拋出這個RejectedExecutionHandler異常

1. 線程池的處理過程

拿第二節創建的線程池來舉例,核心線程是2,最大線程數是5(說明非核心線程數爲3)。任務隊列是ArrayBlockingQueue(特點是它用數組實現,元素排序規則是先進先出,默認不保證線程池按照阻塞的先後順序訪問隊列),數量爲5個,超時爲6秒,其他都爲默認。

那麼,其實內部是這樣的——–>看圖:

  1. 核心線程未飽和
    核心線程未飽和.gif
    當只有1核心線程時,這時新建的任務會直接添加爲核心線程

  1. 核心線程飽和隊列未飽和
    核心線程飽和隊列未飽和.gif
    當核心線程已滿,任務隊列未飽和時,這時新建的任務會添加到工作隊列

  1. 核心線程飽和隊列飽和非核心線程未飽和
    核心線程飽和隊列飽和非核心線程未飽和.gif
    當核心線程已滿,任務隊列已飽和,非核心線程未飽和時,新建的任務會添加爲非核心線程。

  1. 核心線程飽和隊列飽和非核心線程飽和
    核心線程飽和隊列飽和非核心線程飽和.gif

當線程池線程已達最大值,隊列也已飽和,這時新建任務會執行飽和策略


總結起來,其實就是:

線程池執行流程.png


這裏很懵逼的是阻塞隊列,事實上不是每個阻塞隊列都像ArrayBlockingQueue如此,下節將分析常用的阻塞隊列

四、常見阻塞隊列及使用場景

阻塞隊列使用方法大同小異,只要瞭解他的內部結構構成,以及由其結構影響的各種特性即可,具體測試及用法可看
BlockingQueue(阻塞隊列)詳解
深入理解阻塞隊列(二)——ArrayBlockingQueue源碼分析
深入理解阻塞隊列(三)——LinkedBlockingQueue源碼分析

常用的幾種阻塞隊列如下:
- ArrayBlockingQueue:有界阻塞隊列,它用數組實現,元素排序規則是先進先出,默認不保證線程池按照阻塞的先後順序訪問隊列,一般構造方法會指定元素數量,和是否公平順序按照阻塞順序訪問隊列,通常用在需要生產者和消費者順序的操作隊列中的數據,以降低吞吐量的時候,比如
- LinkedBlockingQueue:有界阻塞隊列,鏈表實現,與ArrayBlockingQueue區別不同,它是並行的操作隊列中的數據,這也決定了它能用於高併發,巨大吞吐量的情況,需要注意的是,LinkedBlockingQueue的容量記得要指定哦,不然太大了加入生產者的速度大於消費者,那麼隊列阻塞可能不會阻塞,因爲內存會炸。
- SyschronousQueue:不存儲元素的異步隊列,有個特點,插入操作的完成要等待另一個線程的對應移除操作,適合那種立即處理且耗時較少的任務。

五、比較Executors中3種線程池的區別和使用情景

其實不止3種(有6種),這裏只分析其中常用的4種,其他的大同小異

  1. FixedThreadPool:固定線程數的線程池,特點在覈心線程和線程最大數量相等,意味着只有核心線程,keepAliveTime時間爲0說明多餘線程馬上停止,隊列它用的是new LinkedBlockingQueue<Runnable>(),這裏源碼點進去看發現指定隊列容量爲無窮大。總結的說,就是線程池大小固定,任務隊列無界
/**
     * Creates a thread pool that reuses a fixed number of threads
     * operating off a shared unbounded queue.  At any point, at most
     * {@code nThreads} threads will be active processing tasks.
     * If additional tasks are submitted when all threads are active,
     * they will wait in the queue until a thread is available.
     * If any thread terminates due to a failure during execution
     * prior to shutdown, a new one will take its place if needed to
     * execute subsequent tasks.  The threads in the pool will exist
     * until it is explicitly {@link ExecutorService#shutdown shutdown}.
     *
     * @param nThreads the number of threads in the pool
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code nThreads <= 0}
     */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • 應用場景:保證所有任務都會被執行,永遠不拒絕新任務。但是假如任務時間無限長的時候會出現由於隊列數量過大引起的內存問題。
    1. CacheThreadPool:核心線程爲0,線程最大值爲無窮大,說明
      非核心線程數是無窮大的,空閒線程等待新任務的時間是60s。這裏阻塞隊列用的是new SynchronousQueue<Runnable>()說明每個插入和移除操作要同步進行。總結的說,就是線程池無心大,等待長度爲1(因爲阻塞隊列的原因)
    /**
     * Creates a thread pool that creates new threads as needed, but
     * will reuse previously constructed threads when they are
     * available.  These pools will typically improve the performance
     * of programs that execute many short-lived asynchronous tasks.
     * Calls to {@code execute} will reuse previously constructed
     * threads if available. If no existing thread is available, a new
     * thread will be created and added to the pool. Threads that have
     * not been used for sixty seconds are terminated and removed from
     * the cache. Thus, a pool that remains idle for long enough will
     * not consume any resources. Note that pools with similar
     * properties but different details (for example, timeout parameters)
     * may be created using {@link ThreadPoolExecutor} constructors.
     *
     * @return the newly created thread pool
     */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 應用場景:適合大量的需要立即處理並且耗時較少的的任務
    1. SingleThreadPool:核心線程和最大線程數都爲0,也就是說SingleThreadPool只有一個核心線程,後面的等待時間隊列都和FixedThreadPool一樣。總結的說,就是線程池大小固定爲1,任務隊列無界
/**
     * Creates an Executor that uses a single worker thread operating
     * off an unbounded queue. (Note however that if this single
     * thread terminates due to a failure during execution prior to
     * shutdown, a new one will take its place if needed to execute
     * subsequent tasks.)  Tasks are guaranteed to execute
     * sequentially, and no more than one task will be active at any
     * given time. Unlike the otherwise equivalent
     * {@code newFixedThreadPool(1)} the returned executor is
     * guaranteed not to be reconfigurable to use additional threads.
     *
     * @return the newly created single-threaded Executor
     */
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • 應用場景:它的特性保證所有了所有的任務在一個線程中按順序運行。所以它適用於在邏輯上需要單線程處理任務的場景。由於阻塞隊列無限大,同樣可能會出現FixedThreadPool的耗時過長時產生的內存問題。

六、對比線程和線程池的優缺點,各種使用場景及其區別

我們知道使用線程池可以大大的提高系統的性能,提高程序任務的執行效率,在線程池中,每一個工作線程都能得到重複利用,執行多個任務,減少對象新建回收的次數。

線程缺點:

1、每次新建線程都需要新建對象
2、沒有定時執行,定期執行,中斷
3、不能統一管理,有些時候會發生線程之間對資源的競爭,一定情況下就會內存泄漏。

線程池缺點:

1、一旦加入到線程池中就沒有辦法讓它停止,除非任務執行完畢自動停止;

2、一個進程共享一個線程池;

3、要執行的任務不能有返回值(當然,線程中要執行的方法也是不能有返回值,如果確實需要返回值必須採用其它技巧來解決);

4、在線程池中所有任務的優先級都是一樣的,無法設置任務的優先級;

5、不太適合需要長期執行的任務(比如在Windows服務中執行),也不適合大的任務;

6、不能爲線程設置穩定的關聯標識,比如爲線程池中執行某個特定任務的線程指定名稱或者其它屬性。

線程池優點:

1、重用已存在的線程,減少對象創建、回收次數,提高JVM性能
2、通過控制最大線程數,提高系統資源利用率
3、定時執行、定期執行、併發數控制。

線程池使用場景

實際情況下,Web 服務器、數據庫服務器、文件服務器或郵件服務器之類的許多服務器應用程序都需要用線程池去處理遠程傳過來的任務。

當然,如果說,細化的話,上面已分析過3種線程池的使用情況,其他的和他們三大體上分析過程是一樣的。具體線程池的參數如何分配,需要根據實際需求情況去確定,重要的當然是分析過程咯哦。

2018-05-10 11:30AM
更新中…

參考

用線程池和不用線程池的區別是什麼?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章