造輪子-ThreadPoolExecutor

頂風少年發表於2021-03-05

以下程式碼的實現邏輯出自於公眾號 碼農翻身

《你管這破玩意叫執行緒池?》

- PS:劉欣老師在我心中是軟體技術行業的大劉。

執行緒池介面

造輪子-ThreadPoolExecutor
public interface Executor {
    public void execute(Runnable r);
}
View Code

介面中只有一個抽象方法,execute(Runnable r);它接收一個Runnable,無返回值實現它的子類只需要將傳入的Runnable執行即可。

NewsThreadExecutor

造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run;

import com.datang.bingxiang.run.intr.Executor;

public class NewsThreadExecutor implements Executor {
    //每次呼叫都建立一個新的執行緒
    @Override
    public void execute(Runnable r) {
        new Thread(r).start();
    }

}
View Code

這個實現類最簡單也最明白,真的每次呼叫我們都建立一個Thread將引數Runnable執行。這麼做的弊端就是每個呼叫者釋出一個任務都需要建立一個新的執行緒,執行緒使用後就被銷燬了,對記憶體造成了很大的浪費。

SingThreadExecutor

造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run;

import java.util.concurrent.ArrayBlockingQueue;

import com.datang.bingxiang.run.intr.Executor;

//只有一個執行緒,在例項化後就啟動執行緒。使用者呼叫execute()傳遞的Runnable會新增到佇列中。
//佇列有一個固定的容量3,如果佇列滿則拋棄任務。
//執行緒的run方法不停的迴圈,從佇列裡取Runnable然後執行其run()方法。
public class SingThreadExecutor implements Executor {

    // ArrayBlockingQueue 陣列型別的有界佇列
    // LinkedBlockingDeque 連結串列型別的有界雙端佇列
    // LinkedBlockingQueue 連結串列型別的有界單向佇列
    private ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1);

    //執行緒不停的從佇列獲取任務
    private Thread worker = new Thread(() -> {
        while (true) {
            try {
                //take會在獲取不到任務時阻塞。並且也有Lock鎖
                Runnable r = queue.take();
                r.run();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }
    });

    // 建構函式啟動執行緒
    public SingThreadExecutor() {
        worker.start();
    }

    @Override
    public void execute(Runnable r) {
        // 這個offer和add不同的是offer有Lock鎖,如果佇列滿則返回false。
        // add則是佇列滿丟擲異常,並且沒有Lock鎖。
        if (!queue.offer(r)) {
            System.out.println("執行緒等待佇列已滿,不可加入。本次任務丟棄!");
        }
    }

}
View Code

改變下思路,這次執行緒池實現類只建立一個執行緒,呼叫者釋出的任務都存放到一個佇列中(佇列符合先進先出的需求)但是注意我們設計執行緒池一定要選擇有界佇列,因為我們不能無限制的往佇列中新增任務。在佇列滿後,在進來的任務就要被拒絕掉。ArrayBlockingQueue

是一個底層有陣列實現的有界阻塞佇列,例項化一個ArrayBlockingQueue傳遞引數為1,表示佇列長度最大為1.唯一的一個工作執行緒也是成員變數,執行緒執行後不斷的自旋從佇列中獲取任務,take()方法將佇列頭的元素出隊,若佇列為空則阻塞,這個方法是執行緒安全的。

execute(r)方法接收到任務後,將任務新增到佇列中,offer()方法將元素新增到佇列若佇列已滿則返回false。execute(r)則直接拒絕掉本次任務。

CorePollThreadExecutor

SingThreadExecutor執行緒池的缺點是隻有一個工作執行緒,這樣顯然是不夠靈活,CorePollThreadExecutor中增加了corePollSize核心執行緒數引數,由使用者規定有需要幾個工作執行緒。這次我們選用的佇列為LinkedBlockingQueue這是一個資料結構為連結串列的有界阻塞單向佇列。

initThread()方法根據corePollSize迴圈建立N個執行緒,執行緒建立後同樣呼叫take()方法從阻塞佇列中獲取元素,若獲取成功則執行Runnable的run()方法,若獲取佇列中沒有元素則阻塞。execute(r)則還是負責將任務新增到佇列中。

 

CountCorePollThreadExecutor

CorePollThreadExecutor中有三個問題

1 當佇列滿時執行緒池直接拒絕了任務,這應該讓使用者決定被拒絕的任務如何處理。

2 執行緒的建立策略也應該交給使用者做處理。

3 初始化後就建立了N個核心執行緒數,但是這些執行緒可能會用不到而造成浪費。

RejectedExecutionHandler介面的實現應該讓使用者決定如何處理佇列滿的異常情況。

造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run.intr;

public interface RejectedExecutionHandler {
    public void rejectedExecution();
}
View Code
造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run;

import com.datang.bingxiang.run.intr.RejectedExecutionHandler;

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution() {
        System.out.println("佇列已經滿了!!!!當前task被拒絕");
    }

}
View Code

ThreadFactory介面的實現應該讓使用者決定建立執行緒的方法。

造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run.intr;

public interface ThreadFactory {
    public Thread newThread(Runnable r);
}
View Code
造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run;

import com.datang.bingxiang.run.intr.ThreadFactory;

public class CustomThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        System.out.println("建立了新的核心執行緒");
        return new Thread(r);
    }

}
View Code

CountCorePollThreadExecutor的建構函式接收三個引數corePollSize,rejectedExecutionHandler,threadFactory。因為現在我們需要按需建立核心執行緒,所以需要一個變數workCount記錄當前已經建立的工作執行緒,為了保證執行緒之間拿到的workCount是最新的(可見性),我們需要給變數workCount加上volatile修飾,保證改變了的修改能被所有執行緒看到。execute(r)首先要呼叫initThread(r)判斷是否有執行緒被建立,如果沒有執行緒建立則表示工作執行緒數已經和核心執行緒數相同了,此時需要將新的任務新增到佇列中,如果佇列滿,則執行傳入的拒絕策略。重要的方法在於initThread(r)。initThread(r)方法返回true表示有工作執行緒被建立任務將被工作執行緒直接執行,無需入佇列。返回false則將任務入隊,佇列滿則執行拒絕策略。

fill變數表示核心執行緒數是否全部建立,為了保證多執行緒的環境下不會建立多於corePoolSize個數的執行緒,所以需要使用同步鎖,initThread(r)都要使用鎖則會降低效率,尤其是當工作執行緒數已經到達核心執行緒數後,所以這一塊程式碼使用到了雙重判斷,當加鎖後在此判斷工作執行緒是否已滿。如果已滿返回false。接下來使用threadFactory工廠建立執行緒,線上程中使用程式碼塊,保證當前任務可以被新建立的工作執行緒執行。新的工作執行緒依然是從佇列中獲取任務並執行。執行緒開啟後工作執行緒++,如果工作執行緒數等於核心執行緒數則改變fill標記。返回true,成功建立執行緒,不要忘記在finally中釋放鎖。

造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.datang.bingxiang.run.intr.Executor;
import com.datang.bingxiang.run.intr.RejectedExecutionHandler;
import com.datang.bingxiang.run.intr.ThreadFactory;

public class CountCorePollThreadExecutor implements Executor {

    // 核心執行緒數
    private Integer corePollSize;

    // 工作執行緒數,也就是執行緒例項的數量
    private volatile Integer workCount = 0;

    // 執行緒是否已滿
    private volatile boolean fill = false;

    // 拒絕策略,由呼叫者傳入,當佇列滿時,執行自定義策略
    private RejectedExecutionHandler rejectedExecutionHandler;

    // 執行緒工廠,由呼叫者傳入
    private ThreadFactory threadFactory;

    public CountCorePollThreadExecutor(Integer corePollSize, RejectedExecutionHandler rejectedExecutionHandler,
            ThreadFactory threadFactory) {
        this.corePollSize = corePollSize;
        this.rejectedExecutionHandler = rejectedExecutionHandler;
        this.threadFactory = threadFactory;
    }

    // 這次使用連結串列型別的單向佇列
    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(1);

    @Override
    public void execute(Runnable r) {
        // 如果沒有建立執行緒
        if (!initThread(r)) {
            // offer和ArrayBlockingQueue的offer相同的作用
            if (!queue.offer(r)) {
                rejectedExecutionHandler.rejectedExecution();
            }
        }

    }

    // 同步鎖,因為判斷核心執行緒數和工作執行緒數的操作需要執行緒安全
    Lock lock = new ReentrantLock();

    public boolean initThread(Runnable r) {
        // 如果工作執行緒沒有建立滿則需要建立。
        if (!fill) {
            try {
                lock.lock();// 把鎖 加在判斷裡邊是為了不讓每次initThread方法執行時都加鎖
                // 此處進行雙重判斷,因為可能因為多執行緒原因多個執行緒都判斷工作執行緒沒有建立滿,但是不要緊
                // 只有一個執行緒可以進來,如果後續執行緒二次判斷已經滿了就直接返回。
                if (fill) {
                    return false;
                }
                Thread newThread = threadFactory.newThread(() -> {
                    // 因為執行緒是由任務觸發建立的,所以先把觸發執行緒建立的任務執行掉。
                    {
                        r.run();
                    }

                    while (true) {
                        // 然後該執行緒則不停的從佇列中獲取任務
                        try {
                            Runnable task = queue.take();
                            task.run();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
                newThread.start();
                // 工作執行緒數+1
                workCount++;
                // 如果工作執行緒數已經與核心執行緒數相等,則不可建立
                if (workCount == corePollSize) {
                    fill = true;
                }
                return true;
            } finally {
                lock.unlock();// 釋放鎖
            }
        } else {
            // 工作執行緒已滿則不建立
            return false;
        }

    }

}
View Code

ThreadPoolExecutor

最後考慮下,當工作執行緒數到達核心執行緒數後,佇列也滿了以後,任務就被拒絕了。能不能想個辦法,當工作執行緒滿後,多增加幾個執行緒工作,當任務不多時在將擴充套件的執行緒銷燬。ThreadPoolExecutor的建構函式中新增三個引數maximumPoolSize最大執行緒數keepAliveTime空閒時間,unit空閒時間的單位。

和CountCorePollThreadExecutor相比較在流程上講我們只需要在佇列滿時判斷工作執行緒是否和最大執行緒數相等,如果不相等則建立備用執行緒,並且在備用執行緒長時間不工作時需要銷燬掉工作執行緒。create()方法雙重判斷workCount==maximumPoolSize如果已經相等表示已經不能建立執行緒了,此時只能執行拒絕策略。否則建立備用執行緒,備用執行緒建立後自旋的執行poll(l,u)方法,該方法也是取出佇列頭元素,和take()不同的是,poll如果一段時間後仍然從佇列中拿不到元素(佇列為空)則返回null,此時我們需要將該備用執行緒銷燬。在建立執行緒後將workCount++。此外需要注意,因為當前佇列滿了,所以才會建立備用執行緒所以不要將當前的任務給忘了,LinkedBlockingQueue的put(r)方法會阻塞的新增元素,直到新增成功。最後 stop()判讀如果workCount>corePollSize則線上程安全的環境下將執行緒停止,並且將workCount--。

造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.datang.bingxiang.run.intr.Executor;
import com.datang.bingxiang.run.intr.RejectedExecutionHandler;
import com.datang.bingxiang.run.intr.ThreadFactory;

public class ThreadPoolExecutor implements Executor {

    // 核心執行緒數
    private Integer corePollSize;

    // 工作執行緒數,也就是執行緒例項的數量
    private Integer workCount = 0;

    // 當佇列滿時,需要建立新的Thread,maximumPoolSize為最大執行緒數
    private Integer maximumPoolSize;

    // 當任務不多時,需要刪除多餘的執行緒,keepAliveTime為空閒時間
    private long keepAliveTime;

    // unit為空閒時間的單位
    private TimeUnit unit;

    // 執行緒是否已滿
    private boolean fill = false;

    // 拒絕策略,由呼叫者傳入,當佇列滿時,執行自定義策略
    private RejectedExecutionHandler rejectedExecutionHandler;

    // 執行緒工廠,由呼叫者傳入
    private ThreadFactory threadFactory;

    // 這次使用連結串列型別的單向佇列
    BlockingQueue<Runnable> workQueue;

    public ThreadPoolExecutor(Integer corePollSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
            RejectedExecutionHandler rejectedExecutionHandler) {
        this.corePollSize = corePollSize;
        this.rejectedExecutionHandler = rejectedExecutionHandler;
        this.threadFactory = threadFactory;
        this.workQueue = workQueue;
        this.maximumPoolSize = maximumPoolSize;
        this.keepAliveTime = keepAliveTime;
        this.unit = unit;
    }

    @Override
    public void execute(Runnable r) {
        // 如果沒有建立執行緒
        if (!initThread(r)) {
            // offer和ArrayBlockingQueue的offer相同的作用
            if (!workQueue.offer(r)) {
                // 佇列滿了以後先不走拒絕策略而是查詢執行緒數是否到達最大執行緒數
                if (create()) {
                    Thread newThread = threadFactory.newThread(() -> {
                        while (true) {
                            // 然後該執行緒則不停的從佇列中獲取任務
                            try {
                                Runnable task = workQueue.poll(keepAliveTime, unit);
                                if (task == null) {
                                    stop();
                                } else {
                                    task.run();
                                }
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                    newThread.start();
                    // 工作執行緒數+1
                    workCount++;
                    // 增加執行緒後,還需要將本應該被拒絕的任務新增到佇列
                    try {
                        // 這個put()方法會在佇列滿時阻塞新增,直到新增成功
                        workQueue.put(r);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    rejectedExecutionHandler.rejectedExecution();
                }
            }
        }

    }

    Lock clock = new ReentrantLock();

    private boolean create() {
        //雙重檢查
        if (workCount == maximumPoolSize) {
            return false;
        }
        try {
            clock.lock();
            if (workCount < maximumPoolSize) {
                return true;
            } else {
                return false;
            }
        } finally {
            clock.unlock();
        }

    }

    Lock slock = new ReentrantLock();

    // 銷燬執行緒
    private void stop() {
        slock.lock();
        try {
            if (workCount > corePollSize) {
                System.out.println(Thread.currentThread().getName() + "執行緒被銷燬");
                workCount--;
                Thread.currentThread().stop();
            }
        } finally {
            slock.unlock();
        }

    }

    // 獲取當前的工作執行緒數
    public Integer getworkCount() {
        return workCount;
    }

    // 同步鎖,因為判斷核心執行緒數和工作執行緒數的操作需要執行緒安全
    Lock lock = new ReentrantLock();

    public boolean initThread(Runnable r) {
        // 如果工作執行緒沒有建立滿則需要建立。
        if (!fill) {
            try {
                lock.lock();// 把鎖 加在判斷裡邊是為了不讓每次initThread方法執行時都加鎖
                // 此處進行雙重判斷,因為可能因為多執行緒原因多個執行緒都判斷工作執行緒沒有建立滿,但是不要緊
                // 只有一個執行緒可以進來,如果後續執行緒二次判斷已經滿了就直接返回。
                if (fill) {
                    return false;
                }
                Thread newThread = threadFactory.newThread(() -> {
                    // 因為執行緒是由任務觸發建立的,所以先把觸發執行緒建立的任務執行掉。
                    {
                        r.run();
                    }

                    while (true) {
                        // 然後該執行緒則不停的從佇列中獲取任務
                        try {
                            Runnable task = workQueue.take();
                            task.run();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                    }
                });
                newThread.start();
                // 工作執行緒數+1
                workCount++;
                // 如果工作執行緒數已經與核心執行緒數相等,則不可建立
                if (workCount == corePollSize) {
                    fill = true;
                }
                return true;
            } finally {
                lock.unlock();// 釋放鎖
            }
        } else {
            // 工作執行緒已滿則不建立
            return false;
        }
    }

}
View Code

 

 

測試程式碼

造輪子-ThreadPoolExecutor
package com.datang.bingxiang.run.test;

import java.time.LocalDateTime;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.datang.bingxiang.run.CorePollThreadExecutor;
import com.datang.bingxiang.run.CountCorePollThreadExecutor;
import com.datang.bingxiang.run.CustomRejectedExecutionHandler;
import com.datang.bingxiang.run.CustomThreadFactory;
import com.datang.bingxiang.run.NewsThreadExecutor;
import com.datang.bingxiang.run.SingThreadExecutor;
import com.datang.bingxiang.run.ThreadPoolExecutor;
import com.datang.bingxiang.run.intr.Executor;

@RestController
public class TestController {



    private int exe1Count = 1;
    Executor newsThreadExecutor = new NewsThreadExecutor();

    // 每次都建立新的執行緒執行
    @GetMapping(value = "exe1")
    public String exe1() {
        newsThreadExecutor.execute(() -> {
            System.out.println("正在執行" + exe1Count++);
        });
        return "success";
    }

    /*
     * 等待佇列長度為1,三個執行緒加入,第一個加入後會迅速的出佇列。剩下兩個只有一個可以成功 加入,另一個 則會被丟棄
     */
    private int exe2Count = 1;
    Executor singThreadExecutor = new SingThreadExecutor();

    @GetMapping(value = "exe2")
    public String exe2() {
        singThreadExecutor.execute(() -> {
            System.out.println("正在執行" + exe2Count++);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return "success";
    }

    private int exe3Count = 1;
    Executor corePollThreadExecutor = new CorePollThreadExecutor(2);

    @GetMapping(value = "exe3")
    public String exe3() {
        corePollThreadExecutor.execute(() -> {
            System.out.println("正在執行" + exe3Count++);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return "success";
    }

    private int exe4Count = 1;
    Executor countCorePollThreadExecutor = new CountCorePollThreadExecutor(2, new CustomRejectedExecutionHandler(),
            new CustomThreadFactory());

    @GetMapping(value = "exe4")
    public String exe4() {
        countCorePollThreadExecutor.execute(() -> {
            System.out.println("正在執行" + exe4Count++);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return "success";
    }

    // 第一次建立執行緒並執行 1
    // 第二次進入佇列 2
    // 第三次建立執行緒取出佇列中的2,將3新增到佇列
    // 第四次拒絕
    // 等待3秒後只剩下一個佇列
    private int exe5Count = 1;
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 3, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1), new CustomThreadFactory(), new CustomRejectedExecutionHandler());

    @GetMapping(value = "exe5")
    public String exe5() {
        threadPoolExecutor.execute(() -> {
            System.out.println("正在執行" + exe5Count++);
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return "success";
    }

    @GetMapping(value = "workCount")
    public Integer getWorkCount() {
        return threadPoolExecutor.getworkCount();
    }
}
View Code

 

相關文章