Java 線程池分析

線程作為CPU執行的工作單位,具有自己的上下文環境,也會占用一定的資源。Linux下對于系統的整個線程數量有一定限制,一個進程下的線程數量也是有一定限制的,通常情況下Linux下一個進程下的可執行線程數量默認為1024。因為一臺服務器的資源是有一定空間的,每一條線程都會占用一定的資源,因此對線程數量的把控也是理所應當的事情。既然線程作為一種可利用的資源存在,如何去有效的管理資源就是線程池主要做的事情。下面讓我們仔細看看Java的線程池都做了哪些事情。

線程概念

Java中對于線程有自己的支持,支持用戶自行創建線程,一般我們是通過實現Runnable接口來創建線程,簡單步驟如下:

static class DemoThread implements Runnable{
        public void run() {
            System.out.println("My Name Is :" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        int i = 0;
        while ((i++) < 10) {
            Thread t = new Thread(new DemoThread());
            t.start();
        }
    }

在這里我創建了十個線程并且分別打印出了線程的名字,實現非常簡單。只是通過實現一個Runnable接口然后作為Thread的構造函數的參數傳進去即可,這樣我們就創建了十個線程出來。

線程池

Java中我們一般通過Executors工具類來創建線程,我們日常使用的線程池主要是四類:

  • newFixedThreadPool 固定線程數量的線程池,也就是說如果即使有空閑的線程在線程池中也不會進行回收操作。他的內部實現其實就是令corePoolSize = maximumPoolSize。但是它的任務隊列主要是通過LinkedBlockingQueue來實現的,這個隊列是無界隊列,所以在使用的時候一定要注意,防止隊列無線增長撐爆內存。
  • newCachedThreadPool 一個帶有緩存特性的線程池,如果需要執行任務的時候就會創建出來一個線程,當線程空閑的時候也會回收線程。這個線程池主要適用于執行很多短時間的異步任務。默認的回收時間是60s,線程池的最大線程數量為Integer.MAX_VALUE,它的內部阻塞隊列為SynchronousQueue。
  • newSingleThreadExecutor 一個單線程的線程池,這個線程池與普通的單個線程的區別主要是他可以被重復利用,當這個線程出現異常時候系統會自從創建一個新的線程來代替他執行任務,這個線程池的阻塞隊列也是LinkedBlockingQueue,也是無界隊列。因為LinkedBlockingQueue是有順序的隊列,因此該線程池能保證任務的提交順序與執行順序一致。
  • newScheduledThreadPool 這是個執行定時任務的線程池,在實際的使用過程中用的并不多,一般的定時任務都會使用特殊的框架來執行。

當我們看到這些線程池創建的時候,往往都是創建一個ThreadPoolExecutor類出來,通過參數的配置來區分這些線程池的類型。這個類正是線程池的核心類,下面讓我們看看這個ThreadPoolExecutor類的具體構造:
在明確線程池構建之前,需要了解這幾個重要的參數:

  • corePoolSize 線程池的核心線程數量 當我們往線程池中提交一個任務時,如果線程池中的線程數量小于這個值的話就會創建新的線程出來(盡管這時候線程池中仍然有空閑線程),否則就將新添加的任務放到阻塞隊列中去等待執行。
  • maximumPoolSize 線程池中的最大線程數量 當阻塞隊列添加滿(有界隊列)的情況下,如果繼續提交任務的話就會繼續增加線程知道線程的數量達到maximumPoolSize為止。
  • Handler 當線程池的阻塞隊列滿了并且線程數量達到了maximumPoolSize,這時候線程已經沒有辦法處理更多的任務,那么這時候的處理策略就是特殊的Handler,通常有以下幾種處理策略:
  • AbortPolicy 默認的是丟棄策略,即直接拋出異常。
  • CallerRunsPolicy 用調用者自己的線程來執行任務(例如主線程)
  • DiscardPolicy 不拋出異常,直接丟棄新增加的任務。
  • DiscardOldestPolicy 丟掉最老的還未執行的任務。
  • keepAliveTime 空閑線程的存活時間,超出該時間的話線程被回收。一般情況下只有在線程數量超過corePoolSize的時候該參數才會生效,如果配置了allowCoreThreadTimeOut(true)的話核心線程也會被回收。
  • unit 跟keepAliveTime配合使用的時間單位,通常使用的是秒。
  • workQueue 用來保存待執行任務的任務隊列,我們在上面看到了LinkedBlockingQueue的使用,其實還可以使用ArrayBlockingQueue(有界的數組阻塞隊列,推薦);SynchronousQuene是一個不存儲任何元素的阻塞隊列,通常只有在一個線程調用移除操作之后另外一個線程才能夠進行插入操作。
  • threadFactory 創建線程池的工廠類,這個工廠類的好處就是可以對線程進行定制化,例如給每一個線程都添加固定的屬性值等等。
    以上的屬性就是線程池的核心屬性了,理解了上面的屬性值,我們就一起來看看騎內部的具體實現:

具體實現

首先來看一下重要的屬性

//ctl作為重要的狀態字段,其通過高三位作為線程狀態的描述,低29位作為線程數量的統計功能
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //1110 0000 0000。。。
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1; //0001 1111 1111。。。

    //運行狀態被存儲在高三位中
    private static final int RUNNING    = -1 << COUNT_BITS; //1110 0000。。 可以接受新的任務,可以處理隊列任務
    private static final int SHUTDOWN   =  0 << COUNT_BITS; //0000 0000。。 不可以接受新的任務,但是會繼續處理隊列任務
    private static final int STOP       =  1 << COUNT_BITS; //0010 0000。。不接受新任務,不處理隊列中的任務,中斷正在執行的任務              
    private static final int TIDYING    =  2 << COUNT_BITS; //0100 0000。。所有的任務已經結束,任務線程為0
    private static final int TERMINATED =  3 << COUNT_BITS; //0110 0000。。線程池處于關閉狀態
    
    //對于線程狀態的轉換這里也需要介紹一下:
    // RUNNING -> SHUTDOWN(調用shudown方法)
    // (RUNNING or SHUTDOWN) -> STOP(調用shutdownNow方法) 
    // SHUTDOWN -> TIDYING(當任務隊列和線程池都為空時)
    // STOP -> TIDYING(當線程池為空)
    // TIDYING -> TERMINATED(terminated方法執行完) 
    
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }

    private final BlockingQueue<Runnable> workQueue;//上文提到的阻塞隊列
    private final ReentrantLock mainLock = new ReentrantLock();//控制接近工作線程的鎖
    private final HashSet<Worker> workers = new HashSet<Worker>();//工作線程的集合,只有在持有mainLock的情況下才允許操作這個集合
    private long completedTaskCount;//完成的任務數量
    private volatile ThreadFactory threadFactory;
    private volatile RejectedExecutionHandler handler;
    private volatile long keepAliveTime;
    private volatile boolean allowCoreThreadTimeOut; //在控制失效線程時候是否應該考慮coreThread
    private volatile int corePoolSize;
    private volatile int maximumPoolSize;

通常情況下我們通過線程池執行任務的入口是execute和submit,其中execute方法沒有返回值,而submit方法的執行是否返回值的。下面我們通過execute方法切入到具體的線程執行過程中去講解線程池:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        //如果當前線程數小于corePoolSize則創建新的線程執行任務
        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);
        }
        //否則就新增線程直到maximumPoolSize為止
        else if (!addWorker(command, false))
            reject(command);
    }

從上面的代碼解讀中我們看到多次出現方法addWorker,接下來就看看addWorker方法的具體實現:

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

            // 如果狀態值和workQueue不滿足條件就直接返回false,然后什么也不做
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
            //如果worker數量大于線程最大容量就直接返回,否則就增加一個工作線程
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // 重新獲取ctl狀態,如果前后狀態不一致的話就再次重試整個操作
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            final ReentrantLock mainLock = this.mainLock;
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                mainLock.lock();
                try {
                    // 獲取線程失敗或者線程池在此期間被關閉都有可能導致狀態改變,因此需要多次檢查
                    int c = ctl.get();
                    int rs = runStateOf(c);

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // 如果線程t的狀態是正在運行的狀態就直接拋出異常
                            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;
    }

    private void addWorkerFailed(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (w != null)
                workers.remove(w);
            decrementWorkerCount();
            tryTerminate(); //主要用于觸發terminated狀態后的鉤子函數
        } finally {
            mainLock.unlock();
        }
    }

從上面可以看出來這個Worker類就是線程池中處理線程的主要類,他相當于對Thread的一層包裝,下面看看Worker的具體實現。

//Work類既繼承了AQS,可以非常方便的處理鎖操作;也實現了Runnable接口,可以當做一個任務使用
private final class Worker extends AbstractQueuedSynchronizer implements Runnable

       /** Worker內部的真正線程*/
        final Thread thread;
        /** 初始任務,可有而無 */
        Runnable firstTask;
        /** 線程完成的任務數量 */
        volatile long completedTasks;

        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();
  
                //處理中斷狀態,因為如果線程池是STOP狀態的話,會立即停止正在執行的所有任務在,這個時候應該是可以中斷的?;蛘呷绻麍绦腥蝿盏木€程wt是不可中斷的并且線程狀態>=STOP狀態
                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); //處理線程的一些收尾工作
        }
    }

下面繼續看一下getTask()是如何工作的:

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

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

            // 如果線程池目前不能接受新任務并且任務隊列是空或者線程池不再接受新任務,就減少當前的工作線程
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            boolean timed;      //從隊列中取任務的時候是否會等待一段時間過期

            for (;;) {
                int wc = workerCountOf(c);
                //allowCoreThreadTimeOut作為一項線程池的配置,允許核心線程死亡
                timed = allowCoreThreadTimeOut || wc > corePoolSize;
                //如果線程數量小于最大要求數量并且不設置核心線程過期就什么都不做,這里break的話就不會減少線程,因為現在線程池中沒有任務了
                if (wc <= maximumPoolSize && ! (timedOut && timed))
                    break;
                if (compareAndDecrementWorkerCount(c)) //如果可以減少線程的話,減少之后就直接返回
                    return null;
                c = ctl.get();  // 多次讀取狀態值,因為線程池在運作期間可能會發生狀態改變,如果狀態改變了就要重新操作
                if (runStateOf(c) != rs)
                    continue retry; 
                // 如果任務能夠走到這里,說明在運作過程中線程的狀態還是被改變了,需要重新運行for循環。
            }

            try {
                // 如果可以等待過期了就直接調用Pull,否則調用take。調用take時候如果隊列中沒有任務的話就會一直阻塞知道有任務進來,這里不占用CPU
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true; //一次poll操作可能因為沒有數據導致過期
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
    
    從這里我們就知道為什么線程池中的線程可以一直執行任務,因為在沒有任務的時候任務隊列會為空,而在這是調用阻塞隊列的take方法時候回發生阻塞,這時候線程就被掛起來了。當下次有任務進來的時候線程池,take方法立馬被喚醒,然后繼續上面的循環操作指導下一次take被阻塞!

以上是關于Execute操作的一些內容,下面介紹一下另一種提交任務的方式:submit。

我們常用的線程池操作除了execute之外還有submit操作,對于submit操作會返回一個結果值。我們可以在之后的操作里從這個返回的結果值里面取到線程運行的結果。

//submit操作會返回一個Future對象,我們可以從Future對象中取到這次任務的運行結果
<T> Future<T> submit(Callable<T> task);

實際上在線程提交過后會被封裝成一個FutureTask對象。

FutureTask的主要實現又是依托于內部的一個類Sync,下面我們看看這個Sync:
        private final class Sync extends AbstractQueuedSynchronize //通過繼承AQS從而擁有一定的線程狀態控制力
        
        /**0表示該任務可以運行 */
        private static final int READY     = 0;
        /**1表示該任務正在運行*/
        private static final int RUNNING   = 1;
        /**2表示該任務已經運行過了*/
        private static final int RAN       = 2;
        /**4表示該任務已經被取消*/
        private static final int CANCELLED = 4;

        private final Callable<V> callable;
        /**get()方法但會的結果值 */
        private V result;
        /**任務執行過程中可能產生的異常信息*/
        private Throwable exception;

        /**
         * The thread running task. When nulled after set/cancel, this
         * indicates that the results are accessible.  Must be
         * volatile, to ensure visibility upon completion.(這里感覺英文翻譯的比較完整)
         */
        private volatile Thread runner;
        

然后我們從Submit動作的開始來分析線程池是如何一步一步走下去的:

    // 1.封裝任務 2.執行任務 3.返回結果
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    
    // 1.封裝任務
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    
    public FutureTask(Runnable runnable, V result) {
        sync = new Sync(Executors.callable(runnable, result)); //生成callable的適配器
    }
    所以最終還是返回了一個RunnableFuture對象,只不過在這個對象上做了進一步的包裝.
    
    // 2.執行任務
    什么?竟然又回歸到了execute()方法上去,這些在上文中都有講過。所以實際上最后運行的方法還是Sync中定義的run方法:
    
        void innerRun() {
            //設置線程狀態
            if (!compareAndSetState(READY, RUNNING))
                return;

            runner = Thread.currentThread();
            if (getState() == RUNNING) { // 多次檢查
                V result;
                try {
                    result = callable.call();
                } catch (Throwable ex) {
                    setException(ex);
                    return;
                }
                set(result); //設置結果
            } else {
                releaseShared(0); // 試圖設置狀態來反映共享模式下的一個釋放
            }
        }
        //set方法的具體內容
        void innerSet(V v) {
            for (;;) {
                int s = getState();
                if (s == RAN) //任務已經運行過了表示已經被set過了
                    return;
                if (s == CANCELLED) {
                    releaseShared(0);
                    return;
                }
                if (compareAndSetState(s, RAN)) {
                    result = v;
                    releaseShared(0);
                    done();
                    return;
                }
            }
        }
    
    注意:releaseShared是AQS里面的方法,我對這個類不熟悉,所以在此不進行分析,以免誤人子弟。

Done

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容