【Android高級工程師入職字節跳動java基礎】java併發——Java線程池

一、概述

在我們的開發中經常會使用到多線程。例如在Android中,由於主線程的諸多限制,像網絡請求等一些耗時的操作我們必須在子線程中運行。我們往往會通過new Thread來開啓一個子線程,待子線程操作完成以後通過Handler切換到主線程中運行。這麼以來我們無法管理我們所創建的子線程,並且無限制的創建子線程,它們相互之間競爭,很有可能由於佔用過多資源而導致死機或者OOM。所以在Java中爲我們提供了線程池來管理我們所創建的線程。

線程池的優勢

①降低系統資源消耗,通過重用已存在的線程,降低線程創建和銷燬造成的消耗;

②提高系統響應速度,當有任務到達時,無需等待新線程的創建便能立即執行;

③方便線程併發數的管控,線程若是無限制的創建,不僅會額外消耗大量系統資源,更是佔用過多資源而阻塞系統或oom等狀況,從而降低系統的穩定性。線程池能有效管控線程,統一分配、調優,提供資源使用率;

④更強大的功能,線程池提供了定時、定期以及可控線程數等功能的線程池,使用方便簡單。

二、ThreadPoolExecutor

我們可以通過ThreadPoolExecutor來創建一個線程池。

ExecutorService service = new ThreadPoolExecutor(....);

下面我們就來看一下ThreadPoolExecutor中的一個構造方法。

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

ThreadPoolExecutor參數含義

1. corePoolSize

線程池中的核心線程數,默認情況下,核心線程一直存活在線程池中,即便他們在線程池中處於閒置狀態。除非我們將ThreadPoolExecutor的allowCoreThreadTimeOut屬性設爲true的時候,這時候處於閒置的核心線程在等待新任務到來時會有超時策略,這個超時時間由keepAliveTime來指定。一旦超過所設置的超時時間,閒置的核心線程就會被終止。

2. maximumPoolSize

線程池中所容納的最大線程數,如果活動的線程達到這個數值以後,後續的新任務將會被阻塞。包含核心線程數+非核心線程數。

3. keepAliveTime

非核心線程閒置時的超時時長,對於非核心線程,閒置時間超過這個時間,非核心線程就會被回收。只有對ThreadPoolExecutor的allowCoreThreadTimeOut屬性設爲true的時候,這個超時時間纔會對核心線程產生效果。

4. unit

用於指定keepAliveTime參數的時間單位。他是一個枚舉,可以使用的單位有天(TimeUnit.DAYS),小時(TimeUnit.HOURS),分鐘(TimeUnit.MINUTES),毫秒(TimeUnit.MILLISECONDS),微秒(TimeUnit.MICROSECONDS, 千分之一毫秒)和毫微秒(TimeUnit.NANOSECONDS, 千分之一微秒);

5. workQueue

線程池中保存等待執行的任務的阻塞隊列。通過線程池中的execute方法提交的Runable對象都會存儲在該隊列中。我們可以選擇下面幾個阻塞隊列。

阻塞隊列 說明
ArrayBlockingQueue 基於數組實現的有界的阻塞隊列,該隊列按照FIFO(先進先出)原則對隊列中的元素進行排序。
LinkedBlockingQueue 基於鏈表實現的阻塞隊列,該隊列按照FIFO(先進先出)原則對隊列中的元素進行排序。
SynchronousQueue 內部沒有任何容量的阻塞隊列。在它內部沒有任何的緩存空間。對於SynchronousQueue中的數據元素只有當我們試着取走的時候纔可能存在。
PriorityBlockingQueue 具有優先級的無限阻塞隊列。

我們還能夠通過實現BlockingQueue接口來自定義我們所需要的阻塞隊列。

6. threadFactory

線程工廠,爲線程池提供新線程的創建。ThreadFactory是一個接口,裏面只有一個newThread方法。 默認爲DefaultThreadFactory類。

7. handler

是RejectedExecutionHandler對象,而RejectedExecutionHandler是一個接口,裏面只有一個rejectedExecution方法。當任務隊列已滿並且線程池中的活動線程已經達到所限定的最大值或者是無法成功執行任務,這時候ThreadPoolExecutor會調用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四個內部類實現了RejectedExecutionHandler接口。在線程池中它默認是AbortPolicy,在無法處理新任務時拋出RejectedExecutionException異常

下面是在ThreadPoolExecutor中提供的四個可選值。

可選值 說明
CallerRunsPolicy 只用調用者所在線程來運行任務。
AbortPolicy 直接拋出RejectedExecutionException異常。
DiscardPolicy 丟棄掉該任務,不進行處理。
DiscardOldestPolicy 丟棄隊列裏最近的一個任務,並執行當前任務。

我們也可以通過實現RejectedExecutionHandler接口來自定義我們自己的handler。如記錄日誌或持久化不能處理的任務。

ThreadPoolExecutor的使用
ExecutorService service = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

對於ThreadPoolExecutor有多個構造方法,對於上面的構造方法中的其他參數都採用默認值。可以通過execute和submit兩種方式來向線程池提交一個任務。 execute 當我們使用execute來提交任務時,由於execute方法沒有返回值,所以說我們也就無法判定任務是否被線程池執行成功。

service.execute(new Runnable() {
    public void run() {
        System.out.println("execute方式");
    }
});

submit

當我們使用submit來提交任務時,它會返回一個future,我們就可以通過這個future來判斷任務是否執行成功,還可以通過future的get方法來獲取返回值。如果子線程任務沒有完成,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間後立即返回,這時候有可能任務並沒有執行完。

Future<Integer> future = service.submit(new Callable<Integer>() {

    @Override
    public Integer call() throws Exception {
        System.out.println("submit方式");
        return 2;
    }
});
try {
    Integer number = future.get();
} catch (ExecutionException e) {
                // TODO Auto-generated catch block
    e.printStackTrace();
}

線程池關閉

調用線程池的shutdown()shutdownNow()方法來關閉線程池

shutdown原理:將線程池狀態設置成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程。

shutdownNow原理:將線程池的狀態設置成STOP狀態,然後中斷所有任務(包括正在執行的)的線程,並返回等待執行任務的列表。

中斷採用interrupt方法,所以無法響應中斷的任務可能永遠無法終止。 但調用上述的兩個關閉之一,isShutdown()方法返回值爲true,當所有任務都已關閉,表示線程池關閉完成,則isTerminated()方法返回值爲true。當需要立刻中斷所有的線程,不一定需要執行完任務,可直接調用shutdownNow()方法。

三、線程池執行流程

①如果在線程池中的線程數量沒有達到核心的線程數量,這時候就回啓動一個核心線程來執行任務。

②如果線程池中的線程數量已經超過核心線程數,這時候任務就會被插入到任務隊列中排隊等待執行。

③由於任務隊列已滿,無法將任務插入到任務隊列中。這個時候如果線程池中的線程數量沒有達到線程池所設定的最大值,那麼這時候就會立即啓動一個非核心線程來執行任務。

④如果線程池中的數量達到了所規定的最大值,那麼就會拒絕執行此任務,這時候就會調用RejectedExecutionHandler中的rejectedExecution方法來通知調用者。

四、四種線程池類

Java中四種具有不同功能常見的線程池。他們都是直接或者間接配置ThreadPoolExecutor來實現他們各自的功能。這四種線程池分別是newFixedThreadPool,newCachedThreadPool,newScheduledThreadPool和newSingleThreadExecutor。這四個線程池可以通過Executors類獲取。

1. newFixedThreadPool

通過Executors中的newFixedThreadPool方法來創建,該線程池是一種線程數量固定的線程池。

ExecutorService service = Executors.newFixedThreadPool(4);

在這個線程池中 所容納最大的線程數就是我們設置的核心線程數。 如果線程池的線程處於空閒狀態的話,它們並不會被回收,除非是這個線程池被關閉。如果所有的線程都處於活動狀態的話,新任務就會處於等待狀態,直到有線程空閒出來。

由於newFixedThreadPool只有核心線程,並且這些線程都不會被回收,也就是 它能夠更快速的響應外界請求 。從下面的newFixedThreadPool方法的實現可以看出,newFixedThreadPool只有核心線程,並且不存在超時機制,採用LinkedBlockingQueue,所以對於任務隊列的大小也是沒有限制的。

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

2. newCachedThreadPool

通過Executors中的newCachedThreadPool方法來創建。

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

通過s上面的newCachedThreadPool方法在這裏我們可以看出它的 核心線程數爲0, 線程池的最大線程數Integer.MAX_VALUE。而Integer.MAX_VALUE是一個很大的數,也差不多可以說 這個線程池中的最大線程數可以任意大。

當線程池中的線程都處於活動狀態的時候,線程池就會創建一個新的線程來處理任務。該線程池中的線程超時時長爲60秒,所以當線程處於閒置狀態超過60秒的時候便會被回收。 這也就意味着若是整個線程池的線程都處於閒置狀態超過60秒以後,在newCachedThreadPool線程池中是不存在任何線程的,所以這時候它幾乎不佔用任何的系統資源。

對於newCachedThreadPool他的任務隊列採用的是SynchronousQueue,上面說到在SynchronousQueue內部沒有任何容量的阻塞隊列。SynchronousQueue內部相當於一個空集合,我們無法將一個任務插入到SynchronousQueue中。所以說在線程池中如果現有線程無法接收任務,將會創建新的線程來執行任務。

3. newScheduledThreadPool

通過Executors中的newScheduledThreadPool方法來創建。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

它的核心線程數是固定的,對於非核心線程幾乎可以說是沒有限制的,並且當非核心線程處於限制狀態的時候就會立即被回收。

創建一個可定時執行或週期執行任務的線程池:

ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
service.schedule(new Runnable() {
    public void run() {
        System.out.println(Thread.currentThread().getName()+"延遲三秒執行");
    }
}, 3, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Runnable() {
    public void run() {
        System.out.println(Thread.currentThread().getName()+"延遲三秒後每隔2秒執行");
    }
}, 3, 2, TimeUnit.SECONDS);

輸出結果:

pool-1-thread-2延遲三秒後每隔2秒執行
pool-1-thread-1延遲三秒執行
pool-1-thread-1延遲三秒後每隔2秒執行
pool-1-thread-2延遲三秒後每隔2秒執行
pool-1-thread-2延遲三秒後每隔2秒執行

schedule(Runnable command, long delay, TimeUnit unit):延遲一定時間後執行Runnable任務;

schedule(Callable callable, long delay, TimeUnit unit):延遲一定時間後執行Callable任務;

scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延遲一定時間後,以間隔period時間的頻率週期性地執行任務;

scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit):與scheduleAtFixedRate()方法很類似,但是不同的是scheduleWithFixedDelay()方法的週期時間間隔是以上一個任務執行結束到下一個任務開始執行的間隔,而scheduleAtFixedRate()方法的週期時間間隔是以上一個任務開始執行到下一個任務開始執行的間隔,也就是這一些任務系列的觸發時間都是可預知的。

ScheduledExecutorService功能強大,對於定時執行的任務,建議多采用該方法。

4. newSingleThreadExecutor

通過Executors中的newSingleThreadExecutor方法來創建,在這個線程池中只有一個核心線程,對於任務隊列沒有大小限制,也就意味着這一個任務處於活動狀態時,其他任務都會在任務隊列中排隊等候依次執行

newSingleThreadExecutor將所有的外界任務統一到一個線程中支持,所以在這個任務執行之間我們不需要處理線程同步的問題。

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

五、線程池的使用技巧

需要針對具體情況而具體處理,不同的任務類別應採用不同規模的線程池,任務類別可劃分爲CPU密集型任務、IO密集型任務和混合型任務。(N代表CPU個數)

任務類別 說明
CPU密集型任務 線程池中線程個數應儘量少,如配置N+1個線程的線程池。
IO密集型任務 由於IO操作速度遠低於CPU速度,那麼在運行這類任務時,CPU絕大多數時間處於空閒狀態,那麼線程池可以配置儘量多些的線程,以提高CPU利用率,如2*N。
混合型任務 可以拆分爲CPU密集型任務和IO密集型任務,當這兩類任務執行時間相差無幾時,通過拆分再執行的吞吐率高於串行執行的吞吐率,但若這兩類任務執行時間有數據級的差距,那麼沒有拆分的意義。

尾聲

面試主要考的還是你的基礎知識,需要你對Android技術有一個全局上的把握,具體說起來就太多了,具體複習方案可以參考文章後面的內容。


我個人也總結了一些面試方面的經驗,主要是一些技巧。

  • 1 做好自我介紹和項目總結,把握你發言的主動權

  • 2 搞清楚簡歷上的技術點,兵來將擋水來土掩

  • 3 注意分點答題,思路清晰,也更容易講清楚原理。

  • 4 壓力面下保持冷靜,不要回懟面試官

  • 5 HR面試注意常用技巧,可以提前準備。(給大家整理了一些,需要的可以查看我的【GitHub】)

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