Java入門系列之線程池ThreadPoolExecutor原理分析思考(十五)

前言

關於線程池原理分析請參看《http://objcoding.com/2019/04/25/threadpool-running/》,建議對原理不太瞭解的童鞋先看下此文然後再來看本文,這裏通過對原理的學習我談談對線程池的理解,若有錯誤之處,還望批評指正。

線程池思考

線程池我們可認爲是準備好執行應用程序級任務的預先實例化的備用線程集合,線程池通過同時運行多個任務來提高性能,同時防止線程創建過程中的時間和內存開銷,例如,一個Web服務器在啓動時實例化線程池,這樣當客戶端請求進入時,它就不會花時間創建線程,與爲每個任務都創建線程相比,線程池通過避免一次無限創建線程來避免資源(處理器,內核,內存等)用盡,創建一定數量的線程後,通常將多餘的任務放在等待隊列中,直到有線程可用於新任務。下面我們通過一個簡單的例子來概括線程池原理,如下:

    public static void main(String[] args) {

        ArrayBlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue<>(5);

        ThreadPoolExecutor poolExecutor =
                new ThreadPoolExecutor(2,
                        5, Long.MAX_VALUE, TimeUnit.NANOSECONDS, arrayBlockingQueue);

        for (int i = 0; i < 11; i++) {
            try {
                poolExecutor.execute(new Task());
            } catch (RejectedExecutionException ex) {
                System.out.println("拒絕任務 = " + (i + 1));
            }
            printStatus(i + 1, poolExecutor);
        }
    }

    static void printStatus(int taskSubmitted, ThreadPoolExecutor e) {
        StringBuilder s = new StringBuilder();
        s.append("工作池大小 = ")
                .append(e.getPoolSize())
                .append(", 核心池大小 = ")
                .append(e.getCorePoolSize())
                .append(", 隊列大小 = ")
                .append(e.getQueue().size())
                .append(", 隊列剩餘容量 = ")
                .append(e.getQueue().remainingCapacity())
                .append(", 最大池大小 = ")
                .append(e.getMaximumPoolSize())
                .append(", 提交任務數 = ")
                .append(taskSubmitted);

        System.out.println(s.toString());
    }

    static class Task implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }
    }

如上例子很好的闡述了線程池基本原理,我們聲明一個有界隊列(容量爲5),實例化線程池的核心池大小爲2,最大池大小爲10,創建線程沒有自定義實現,默認通過線程池工廠創建,拒絕策略爲默認,提交11個任務。在啓動線程池時,默認情況下它將以無線程啓動,當我們提交第一個任務時,將產生第一個工作線程,並將任務移交給該線程,只要當前工作線程數小於配置的核心池大小,即使某些先前創建的核心線程可能處於空閒狀態,也會爲每個新提交的任務生成一個新的工作線程(注意:當工作線程池大小未超過核心池大小時以創建的Worker中的第一個任務執行即firstTask,而繞過了阻塞隊列),若超過核心池大小會將任務放入阻塞隊列,一旦阻塞隊列滿後將重新創建線程任務,若任務超過最大線程池大小將執行拒絕策略。當阻塞隊列爲無界隊列(如LinkedBlockingQueue),很顯然設置的最大池大小將無效。我們再來闡述下,當工作線程數達到核心池大小時,若此時提交的任務越來越多,線程池的具體表現行爲是什麼呢?

1、只要有任何空閒的核心線程(先前創建的工作線程,但已經完成分配的任務),它們將接管提交的新任務並執行。

2、如果沒有可用的空閒核心線程,則每個提交的新任務都將進入已定義的工作隊列中,直到有一個核心線程可以處理它爲止。如果工作隊列已滿,但仍然沒有足夠的空閒核心線程來處理任務,那麼線程池將恢復而創建新的工作線程,新任務將由它們來執行。 一旦工作線程數達到最大池大小,線程池將再次停止創建新的工作線程,並且在此之後提交的所有任務都將被拒絕。

由上述2我們知道,一旦達到核心線程大小就會進入阻塞隊列(阻塞隊列未滿),我們可認爲這是一種執行阻塞隊列優先的機制,那我們是不是可以思考一個問題:何不創建非核心線程來擴展線程池大小而不是進入阻塞隊列,當達到最大池大小時才進入阻塞隊列進行排隊,這種方式和默認實現方式在效率和性能上是不是可能會更好呢? 但是從另外一個層面來講,既然不想很快進入阻塞隊列,那麼何不將指定的核心池大小進行擴展大一些呢?我們知道線程數越多那麼將導致明顯的數據爭用問題,也就是說在非峯值系統中的線程數會很多,所以在峯值系統中通過創建非核心線程理論上是不是能夠比默認立即進入阻塞隊列具有支撐規模化的任務更加具有性能上的優勢呢?那麼我們怎樣才能修改默認操作呢?我們首先來看看在執行任務時的操作

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

第一步得到當前工作線程數若小於核心池大小,那麼將創建基於核心池的線程然後執行任務,這一點我們沒毛病,第二步若工作線程大小超過核心池大小,若當前線程正處於運行狀態且將其任務放到阻塞隊列中,若失敗進行第三步創建非核心池線程,通過源碼分析得知,若核心池中線程即使有空閒線程也會創建線程執行任務,那麼我們是不是可以得到核心池中是否有空閒的線程呢,若有然後才嘗試使其進入阻塞隊列,所以我們需要重寫阻塞隊列中的offer方法,添加一個是否有空閒核心池的線程,讓其接待任務。所以我們繼承上述有界阻塞隊列,如下:

public class CustomArrayBlockingQueue<E> extends ArrayBlockingQueue {

    private final AtomicInteger idleThreadCount = new AtomicInteger();

    public CustomArrayBlockingQueue(int capacity) {
        super(capacity);
    }

    @Override
    public boolean offer(Object o) {
        return idleThreadCount.get() > 0 && super.offer(o);
    }
}

但是不幸的是,通過對線程池源碼的分析,我們並不能夠得到空閒的核心池的線程,但是我們可以跟蹤核心池中的空閒線程,在獲取任務方法中如下:

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

如上截取獲取任務的核心,若工作線程大小大於核心池大小時,默認情況下會進入阻塞隊列此時通過pool獲取阻塞隊列中的任務,若工作線程大小小於核心池大小時,此時會調用take方法獲從阻塞隊列中獲取可用的任務,此時說明當前核心池線程處於空閒狀態,如果隊列中沒有任務,則線程將在此調用時會阻塞,直到有可用的任務爲止,因此核心池線程仍然處於空閒狀態,所以我們增加上述計數器,否則,調用方法返回,此時該線程不再處於空閒狀態,我們可以減少計數器,重寫take方法,如下:

@Override
public Object take() throws InterruptedException {
    idleThreadCount.incrementAndGet();
    Object take = super.take();
    idleThreadCount.decrementAndGet();
    return take;
}

接下來我們再來考慮timed爲true的情況,在這種情況下,線程將使用poll方法,很顯然,進入poll方法的任何線程當前都處於空閒狀態,因此我們可以在工作隊列中重寫此方法的實現,以在開始時增加計數器,然後,我們可以調用實際的poll方法,這可能導致以下兩種情況之,如果隊列中沒有任務,則線程將等待此調用以提供所提供的超時,然後返回null。到此時,線程將超時,並將很快從池中退出,從而將空閒線程數減少1,因此我們可以在此時減少計數器,否則由方法調用返回,因此該線程不再處於空閒狀態,此時我們也可以減少計數器。

@Override
public Object poll(long timeout, TimeUnit unit) throws InterruptedException {
    idleThreadCount.incrementAndGet();
    Object poll = super.poll(timeout, unit);
    idleThreadCount.decrementAndGet();
    return poll;
}

通過上述我們對offer、pool、take方法的重寫,使得在沒有基於核心池的空閒線程進行擴展非核心線程,還未結束,若達到了最大池大小,此時我們需要將其添加到阻塞隊列中排隊,所以最終使用我們自定義的阻塞隊列,並使用自定義的拒絕策略,如下:

CustomArrayBlockingQueue<Runnable> arrayBlockingQueue = new CustomArrayBlockingQueue<>(5);

ThreadPoolExecutor poolExecutor =
        new ThreadPoolExecutor(10,
                100, Long.MAX_VALUE, TimeUnit.NANOSECONDS, arrayBlockingQueue
                , Executors.defaultThreadFactory(), (r, executor) -> {
            if (!executor.getQueue().add(r)) {
                System.out.println("拒絕任務");
            }
        });

for (int i = 0; i < 150; i++) {
    try {
        poolExecutor.execute(new Task());
    } catch (RejectedExecutionException ex) {
        System.out.println("拒絕任務 = " + (i + 1));
    }
    printStatus(i + 1, poolExecutor);
}

上述我們實現自定義的拒絕策略,將拒絕的任務放入到阻塞隊列中,若阻塞隊列已滿而不能再接收新的任務,我們將調用默認的拒絕策略或者是其他處理程序,所以在將任務添加到阻塞隊列中即調用add方法時,我們還需要重寫add方法,如下:

@Override
public boolean add(Object o) {
    return super.offer(o);
}

總結

以上詳細內容只是針對線程池的默認實現而引發的思考,通過如上方式是否能夠對於規模化的任務處理起來在性能上有一定改善呢?可能也有思慮不周全的地方,暫且分析於此。

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