java執行緒池原始碼一窺

Edson發表於2022-02-25

什麼是執行緒池:

執行緒池是一種執行緒的使用模式,預先將執行緒建立出來採用池化的方式儲存起來,使用的時候直接使用即可,避免頻繁的執行緒的建立與銷燬帶來的效能開銷,也可以對執行緒的數量進行限制管理,避免建立過多的執行緒導致oom異常。

java中使用多執行緒的方式

1,繼承Thread類,實現Runable/Callable介面,呼叫start/call方法便可以開啟一個執行緒。

2,就是我們要說的執行緒池的方式了。

1的方式不用說頻繁建立銷燬執行緒帶來效能開銷,以及執行緒的數量得不到限制管理實際專案中肯定不能用該方式使用多執行緒。

java中的執行緒池Executor框架

他是java對執行緒池的實現,實現了將執行緒的執行單元和分開單元分開這種機制.該框架包括三大部分

1,任務單元即實現Runable/Callable介面的任務類(run/call方法中寫的是你真正想幹的事)。

2,執行單元,快取線上程池中的執行緒來執行你的任務.

3,非同步計算的結果,當任務是實現callable介面時候,執行方法會將返回結果封裝倒Future物件中返回

前置思考:

有一個很經典的面試題問道:執行緒可以呼叫多次start方法麼?答案是不能,為什麼我們可以開啟原始碼

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        /**
          他判斷了執行緒的狀態狀態是0也就是NEW的狀態才能執行該方法,
          很顯然執行緒呼叫過一次start方法後狀態肯定不會為NEW而是TERMINATED,
          所以多次呼叫會丟擲異常。
          至於執行緒的那幾個狀態在內部列舉類State裡面有列舉這裡不再贅述
        */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

好那麼我們不禁要思考執行緒池能夠完成對執行緒的複用,那麼他是如何做到讓執行緒執行完任務之後在那裡不結束等著下一個任務來了繼續去執行的呢?要我們來做的話怎麼做呢?我們可以大膽的設想一下是不是像生產者消費者那樣的模型就可以呢?。

使用執行緒池:

執行緒池的使用並不複雜,有兩種方式去使用他:

1,Excutetors工具類(執行緒池工廠類?)建立一些特定的執行緒池,這裡面是幫你遮蔽了一些引數的設定,直接用他獲取執行緒池物件會有如下問題:他設定的預設引數對你的業務需求來說不見得合理,弄不好就OOM,對你瞭解執行緒池的原理也不是好事兒.所以專案中我們們用下一種方式.具體裡面有幾種執行緒池讀者有興趣可以自行去研究,專案開發中最好不要使用該種方式建立執行緒池.

2, ThreadPoolExecutor建立執行緒池.

/**corePoolSize:核心執行緒數量
   maximumPoolSize:最大執行緒數量
   keepAliveTime:執行緒多長時間沒活幹之後銷燬(預設只針對非核心執行緒,但是可以通過allowCoreThreadTimeOut設定核心執行緒超時銷燬)
   unit:時間單位
   workQueue:快取任務的阻塞佇列(當任務過來時候發現核心執行緒都在忙著就會先快取進去該佇列)
   threadFactory:執行緒工廠
   handler:拒絕策略(當任務無法被執行且不能被快取時候執行)
*/
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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
執行緒池大致邏輯(摘自百度圖片)

執行緒池執行邏輯(摘自百度圖片)

執行緒池中的執行緒是什麼時候建立的:

我們使用執行緒池是直接呼叫execute方法執行任務的.

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        
        int c = ctl.get();
             //這裡執行緒池的設計者巧妙的用1個int型別的變數表示了執行緒池的狀態和當前執行緒的數量
            //二進位制的前3位表示執行緒池狀態後29位表示執行緒的數量
        if (workerCountOf(c) < corePoolSize) {
            //如果當前的執行緒數量小於核心執行緒數量,嘗試新增一個核心執行緒去執行當前任           
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            //如果執行緒池現在是runing的狀態,且入隊成功
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                //double check執行緒池狀態,如果此時執行緒池狀態不是running移除新增的任務並執行拒絕策略
                reject(command);
            else if (workerCountOf(recheck) == 0)
                //如果此時工作中的執行緒數量為0新增一個非核心執行緒
                addWorker(null, false);
        }
       //入隊也沒吃成功新增非核心執行緒
        else if (!addWorker(command, false))
            reject(command);
    }

addWorker方法:

Worker是執行緒池的內部類,裡面封裝了一個執行緒物件,他本身又實現了runable介面,封裝的這個執行緒就是任務的執行單元.這個執行緒在例項化的時候傳的又是當前的Worker物件.有點繞多品品。

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

        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;
                //因為執行緒池採用的是hashset為保證執行緒安全,加鎖
                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();
                        //將worker新增到執行緒池中
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            //滾動更新池中的最大執行緒數
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //這裡就是執行任務的邏輯了,上面提到這個t是worker裡面的一個成員,他的例項化傳了worker物件,所以實際上這裡執行的邏輯應該是worker實現runable介面後複寫的run方法而worker類的run方法又是呼叫的執行緒池中的RunWorker方法
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        //拿到worker裡面封裝的task
        Runnable task = w.firstTask;
        //將worker裡面的task清空,這裡變數名也取的很好,第一個任務,意思是worker第一個執行的任務肯定是當初例項化他傳進去的runable型別的引數,當然這個任務也可能為空,比如執行緒池建立之後執行預先建立執行緒的方法prestartAllCoreThreads
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                //如果task不為空或者getTask不為空就去執行task
                //我想大家已經猜到了,這個getTask多半就是從阻塞佇列中獲取任務了
                //阻塞佇列有什麼特點?沒有任務他就會阻塞在這裡,所以這便可以回答前文的問題,
                //他是靠阻塞佇列的take方法的阻塞特性讓執行緒掛起從而完成執行緒的複用的
                w.lock();
                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 {
            //這個方法是銷燬worker的方法,裡面有將worker從池hashset中移除的邏輯,那麼他什麼時候會走到呢?
            //1,上述邏輯發生了異常即completedAbruptly=true.
            //2,上述邏輯正常退出,咦,剛不是說上述迴圈條件會卡在take方法阻塞住麼,怎麼會正常退出呢?我們看看getTask方法
            processWorkerExit(w, completedAbruptly);
        }
    }
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);

            //這裡意思是說檢查超時的必要條件要麼是核心執行緒也允許超時要麼是當前有非核心執行緒在執行著
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            //忽略第一個極端條件超過最大執行緒數量不看,第一次進來是肯定不會進去這個分支的因為timeout為false.
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                //cas減少一個執行單元的數量,並沒有銷燬執行緒池中的執行緒物件,銷燬動作在processWorkerExit方法中即將執行緒(worker)從池即hashset中移除
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                //這裡就是阻塞獲取佇列裡面的任務
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    //獲取到了任務
                    return r;
                //超時沒有獲取到timeout就是true了,再次迴圈就會到上面減少執行執行緒數量的分支去了
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

最後的問題:

現在我們大致知道了執行緒池的原理,可是還有一個很棘手的問題,就是那幾個引數應該如何設定才合理?在回答這個問題之前不妨先考慮一下為什麼要用多執行緒,答案相信大家應該都知道為了提高cpu的利用率,我們又知道單核處理器在同一時間只會處理一個任務的,之所以我們可以邊看知乎邊聽歌,得益於cpu的時間片機制在各個(程式)執行緒之間不斷的來回切換執行讓我們產生了同時執行的錯覺。切執行緒的上下文切換的開銷也不小,因為系統有使用者態到核心態的切換。

我們主要看核心執行緒數量的引數設定

所以既然上下文切換有開銷,所以執行緒並不是越多越好,在選擇多執行緒時候要對具體的任務具體的分析:

CPU密集型任務:即該任務本來就是需要CPU大量參與的計算型任務,CPU的利用率已經很充分了,這個時候你的執行緒你再弄多的執行緒也只會增加執行緒上下文帶來的額外開銷罷了。所以此時應該理論上設定的執行緒數量和系統cpu數量相同,但是為了防止一些意外的發生一般設定為cpu數量+1個執行緒。

IO密集型任務:任務耗時主要體現線上程等待io操作的返回比如網路呼叫,檔案讀寫之類的,這個時候cpu的利用率並沒有得到充分的使用,所以理論上某種程度來說執行緒數應該是越多越好比如cpu數量的兩倍.

關於引數的設定,經驗性較強,因為你的伺服器上不可能就執行你一個應用,我認為只需明白什麼樣的任務型別怎麼樣去設定,在這個大的思想下自己靈活變通就可以。

總結:

執行緒池的思想可以用大白話來解釋就是公司專案組有5個程式設計師(核心執行緒數)平時的工作就是解決測試所提交的bug,測試會在一個bug管控平臺(阻塞佇列)上提交bug報告,閒的時候bug沒有很多程式設計師做在電腦前帶薪摸摸魚等待測試提交新的bug(阻塞獲取新的任務),忙的時候五個程式設計師996都忙不過來啦,bug管控平臺都快因為提交bug的測試太多,登入都登入不了啦,這個時候老闆說這樣吧,我去招幾個外包過來,外包過來了也幫著專案組在解決bug,但是這個時候還是有很多測試有bug要提交提交不上去,專案組老大怒了,說你們會不會用老子寫的功能,你這提的(ie8不能正常顯示)算什麼bug,滾蛋(拒絕策略)專案開始閒下來了,bug也沒有那麼多了外包也就捲鋪蓋走人了(執行緒超時銷燬).

相關文章