讀懂Java線程池

Java容器

在這裏插入圖片描述

在Java中,容器共有2大類,一種是Collection,另一種是map。其中Collection下又有2中集合類型,一種是普通存儲數據類的list以及set,還有一種是爲高併發服務的queue。爲什麼要提一下容器呢,因爲我們下面提到的線程池將會用到這裏的queue。

線程池

在Java中有兩種線程池

  • ThreadPoolExecutor
    自定義線程池,可以定義線程數量,等待隊列,拒絕策略等
  • ForkJoinPool
    Fork/Join框架是Java 7提供的一個用於並行執行任務的框架,是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。

主要的區別

  1. 使用場景不同,ThreadPoolExecutor可以自定義任務,ForkJoinPool適合具有父子關係的任務,適合分治算法
  2. 使用方法不同,ThreadPoolExecutor需要自定義線程池大小,等待隊列具體類型(ArrayQueue需要指定queue長度,LinkQueue長度則爲int的最大值),ForkJoinPool線程數量默認是CPU的核心數,等待隊列近乎無限,有造成OOM的風險

後面線程池以ThreadPoolExecutor爲基數進行討論。

Executor、Future、FutureTask

Executor

Executor接口,是線程池的根接口:
在這裏插入圖片描述
下面我們來看下ThreadPoolExecutor的類關係圖:
在這裏插入圖片描述
從第一幅截圖中我們可以看到Executor接口中定義了一個execute方法,看方法名也很好理解,用來執行task,同時返回值類型爲void。

下面我們簡單看下其子接口ExecutorService中又定義了哪些內容:

void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

很好理解,定義了一些shutdown方法,以及執行單個task,多個task的方法。

我們以submit爲例:
我們一定可以看到方法的入參是Callable,或者Runnable,其方法返回值是個Future,這都啥玩樣兒?
不急,我們看源碼。

首先我們看下Runnable:

public interface Runnable {
    public abstract void run();
}

再看下Callable:

public interface Callable<V> {
    V call() throws Exception;
}

Future

至此,我們就可以弄清楚了,Runnable執行run方法,沒有返回值,Callable執行的是call方法,這個是有返回值的,也就是說通過call方法,我們就可以拿到線程執行task的結果了。放到線程池中,對應的返回值類型就是個Future,那這個Future又是個啥呢?

public interface Future<V> {
	boolean cancel(boolean mayInterruptIfRunning);
	boolean isCancelled();
	boolean isDone();
	V get() throws InterruptedException, ExecutionException;
	V get(long timeout, TimeUnit unit)  throws InterruptedException, ExecutionException, TimeoutException;
}

根據接口定義,我們可以很清楚的知道了,Future封裝了thread的運行狀態,以及thread運行後的返回值,通過get方法,就可以拿到線程的執行結果。但是,請注意,這個get方法是阻塞的,如果線程內部有死循環等問題,線程將會一直堵塞下去,風險係數非常高。
建議使用其子類CompletableFuture,JDK已經封裝了很多方便的異步邏輯,具體使用線程池這部分就不多做介紹了,後面有機會再寫。

FutureTask

public class FutureTask<V> implements RunnableFuture<V> {
	private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
    private Callable<V> callable;
    private Object outcome; // non-volatile, protected by state reads/writes
    private volatile Thread runner;
    private volatile WaitNode waiters;
    .
    .
    .
}

摘抄部分,可以看到FutureTask即實現了接口RunnableFuture,又持有Callable對象,可以認爲FutureTask同時擁有Runnable和Callable特性。

ThreadPoolExecutor

終於講到ThreadPoolExecutor了,我們先看到pool的構造器,以參數最全的那個爲例:

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:核心線程數,這部分的線程就算空閒,也不會歸還給os
maximumPoolSize:最大線程數,當任務量太多,核心線程處理不過來的時候,會動態擴容到的最大線程數量
keepAliveTime:線程最大空閒時間,空閒超過該時間,該線程歸還給os
unit:時間單位
workQueue:線程阻塞queue,用來緩存暫時處理不過來的任務
threadFactory:線程工廠,用來定義以什麼樣的方式創建線程
handler:拒絕策略,當任務超過最大線程數之後,對後續任務怎麼做處理。jdk自帶4種策略,建議自定義。

執行過程

在這裏插入圖片描述
假設我們現在已經創建出了一個線程池,核心線程數2,最大線程數4,任務隊列大小2。現在有7個task需要被執行,過程如下:

  • task1:此時線程池內沒有線程,首先創建出一個線程thread1來執行task1
  • task2:核心線程數爲2,現有線程數爲1,沒有超過設定值,創建線程thread2來執行task2
  • task3:核心線程數2,現有線程數2,已經達到設定值,且等待隊列爲空,將task3放入等待隊列
  • task4:與task3類似,等待隊列沒有滿,將task4放入等待隊列
  • task5:核心線程數已滿,等待隊列已滿,但是最大線程數爲4,現有線程爲2,則創建線程thread3執行task5
  • task6:與task5類似,還沒有達到最大線程數,創建thread4執行task6
  • task7:核心線程已滿,最大線程已滿,等待隊列已滿,線程池無法消化該任務,執行拒絕策略。

實現原理

我們以submit爲例,探討下線程池工作原理:
首先是入口方法:

public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
}

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
 }

首先先將要執行的task封裝爲RunnableFuture對象ftask(實際是new了一個FutureTask),這個對象本身是Runnable接口的實例,所以可以直接將ftask扔給execute方法,執行task,同時將ftask返回(即實際返回類型是個FutureTask)。

下面再看下execute方法:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;   // 32-3 = 29
private static final int CAPACITY   = (1 << COUNT_BITS) - 1; //最大線程數 2^29 -1 
private static final int RUNNING    = -1 << COUNT_BITS;		//線程池運行狀態
private static final int SHUTDOWN   =  0 << COUNT_BITS;		// 線程池關閉狀態,不接受新任務,會處理完已經接受的任務
private static final int STOP       =  1 << COUNT_BITS;		// 立即關閉線程池,中斷正在處理的任務
private static final int TIDYING    =  2 << COUNT_BITS;		// 所有任務已完成
private static final int TERMINATED =  3 << COUNT_BITS;

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
            
        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);
    }

從上面的源碼中可以看出,線程池的狀態以及數量,都跟COUNT_BITS這個變量有關係:

  • COUNT_BITS:使用了int 32位的後29位,
  • RUNNING: -1 << COUNT_BITS
    -1在Java底層是由32個1表示的,左移29位的話,即111 00000 00000000 00000000 00000000,也就是低29位全部爲0,高3位全部爲1的話,表示RUNNING狀態,即-536870912;
  • SHUTDOWN = 0 << COUNT_BITS
    0在Java底層是由32個0表示的,無論左移多少位,還是32個0,即000 00000 00000000 00000000 00000000,也就是低29位全部爲0,高3位全部爲0的話,表示SHUTDOWN狀態,即0;
  • STOP = 1 << COUNT_BITS
    1在Java底層是由前面的31個0和1個1組成的,左移29位的話,即001 00000 00000000 00000000 00000000,也就是低29位全部爲0,高3位爲001的話,表示STOP狀態,即536870912;
  • 後面還有兩個也類似,就不寫了

從上面的解釋我們便可以看出,ctl的前3位表示的是當前線程池的運行狀態,後29位代表是當前線程池中活動線程的數量,每當新增或者減少,只需要改動後29位中的值就可以了。

當有了這層理解之後,再看下面的execute方法就很容易了:

  1. 獲取當前的ctl
  2. 根據ctl的後29位,判斷當前線程數是否小於核心線程數,如果是,執行addWorker,直接返回
  3. 如果核心線程都被佔滿了,判斷當前線程池是否是running狀態,如果是,則通過offer方法,將當前任務添加至任務隊列
  4. 再次嘗試執行當前task,從緩存queue中取出該task,如果當前核心線程還是被佔着,則調用addWorker,傳入false參數,表示創建非核心線程
  5. 如果在第三步的時候,queue已經滿了,無法添加,則直接調用addWorker啓動非核心線程執行,如果執行失敗,則執行拒絕策略。

所以,任務運行的核心就在於addWorker方法,我們一起來看下其源碼,代碼比較多,主要有兩塊內容,我們先看其中的第一部分:

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        
		// 後面是第二部分,等下再講
       .
       .
       . 
    }
    
private boolean compareAndIncrementWorkerCount(int expect) {
        return ctl.compareAndSet(expect, expect + 1);
}

通過上面的解釋,我們大概可以知道addWorker方法的功能,即在線程池中添加線程來執行,這一部分的內容寫了兩層自旋,在多線程環境下,通過一層層的狀態判斷,最終目的就是爲了執行compareAndIncrementWorkerCount方法,也就是在後29位的數字基礎上+1,表示當前活躍的線程數量+1。

也就是,只有在判斷線程池中能夠創建新線程,之後,才能去執行task,這個也很好理解。

下面看第二部分:

		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 {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
}

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

主要的邏輯有下面幾步:

  1. 將當前任務封裝爲一個Worker,怎麼封裝的呢,見Worker的構造器,很簡單,在創建線程池的時候,有一個參數代表的是如何創建thread的ThreadFactory,創建出一個線程之後,將當前的任務綁定到該thread
  2. 執行:workers.add(w); 將新建的worker放入workers集合中去
  3. 添加成功之後,執行:t.start();執行當前worker綁定的線程

到此處,我們往線程池中添加任務,線程池執行任務,就完成了。

線程複用

等等,似乎少了點什麼,是什麼呢?
我們回顧下上面的過程,在上面的過程中,線程總是被ThreadFactory創建出來,然後綁定task,然後在執行task。
是不是跟我們鎖理解的線程池有點不一樣呢?我們使用線程池的目的就是爲了實現線程的複用,而目前看下來,線程池所做的事情,是在不停的new線程,什麼時候複用的呢??

是不是很困惑呢?

我們知道,Thread.start()只能調用一次,一旦這個調用結束,則該線程就到了stop狀態,不能再次調用start。

所以要實現複用,只能是在run方法裏面做文章了,線程池是怎麼做的呢?

public void run() {
            runWorker(this);
 }

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 pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                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);
        }
    }

由此我們可以看到,線程的run實際最終執行的是runWorker方法,看到了嗎,while 循環!是不是很興奮?
沒錯,當線程創建出來之後,如果當前線程上有綁定task,則執行當前task,如果沒有綁定task,則根據getTask方法去獲取task,然後執行task。

再追蹤下getTask方法:

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

我們看到了什麼?workQueue.poll !!任務隊列啊,簡直熱淚盈眶。

至此,我們便弄清楚了線程池是如何創建線程以及線程複用的。

  1. 如果線程池爲空,或者活躍的線程數量<核心線程數,或者緩存隊列已滿,但活躍線程數量沒有超過最大線程數時,創建新線程,在創建線程的過程中,綁定當前的task,並執行
  2. 當前task執行完畢之後,該線程並沒有退出,而是再次從任務緩存列隊中取出第一條task繼續執行,只要能獲取到獲取到任何一條task,當前Thread都不會退出,會一直執行下去
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章