Java不魔改默認線程池機制來實現可伸縮的線程池

java的線程池提供設置基本大小和最大大小兩個參數來實現可伸縮的線程池

   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)

可java的默認實現有時候卻並不是我們想要的,在這個默認實現中,線程池創建之初,線程池會創建 corePoolSize 個數量的線程,這些線程不停的處理 workQueue 中的任務, 當 workQueue 被填滿,當前所有線程全部處於忙碌狀態,已經無法騰出時間來處理新任務,線程池纔會創建新的線程來處理任務, 也就是說,如果workQueue未被填滿,線程池是不會創建新線程的。 那麼,當 workQueue 是個無界隊列時,maximumPoolSize 參數是無效的。 而當 workQueue 是有界隊列時,假如即使 maximumPoolSize 個線程也無法滿足任務處理的速率,則那些未能被處理的任務將被飽和策略退回,有時候這並不符合我們的預期。 所以, 真正的能滿足我們需求的可伸縮線程池需要我們自己實現。

在這裏, 我講解兩種方法

第一種比較簡單,只需要單純的設置默認線程池的參數即可實現

public class CustomThreadPool implements Executor {
    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    ThreadPoolExecutor threadPoolExecutor;

    public CustomThreadPool(int nThreads, int keepAliveTime) {
        threadPoolExecutor = new ThreadPoolExecutor(nThreads, nThreads, keepAliveTime, TimeUnit.SECONDS, queue);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
    }

    @Override
    public void execute(@NotNull Runnable command) {
        threadPoolExecutor.execute(command);
    }
}

我們通過默認線程池構造函數初始化一個線程池, 將 corePoolSize 和 maximumPoolSize 的值設置成相同, 然後再設置空閒線程的回收時間,這樣我們便創建了一個固定大小的線程池。 然而,此時線程池空閒的線程是無法被回收的,因爲可以被回收的線程只能是 maximumPoolSize 減去 corePoolSize 個數量的線程, 此時這個數量爲0,自然也就不會有線程被回收。

但是我們可以

  threadPoolExecutor.allowCoreThreadTimeOut(true); 

通過設置這個值來取消上面的限制, 當allowCoreThreadTimeOut 設置爲true,無關 corePoolSize 的大小, 只要滿足回收條件就都可以被回收, 如果所有線程都空閒,則所有線程都會被回收。

測試代碼如下

CustomThreadPool customThreadPool = new CustomThreadPool(10,60);

int count = 0;

while (count < 200) {
  customThreadPool.execute(() -> {
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("hello world");
  });
  count++;
}

這種方法的好處是簡單,壞處是線程的創建和回收頻率比較高

第二種方法克服了第一種方法存在的問題,可是需要一條額外的線程來管理任務

public class ElasticityThreadPool implements Executor {
    SynchronousQueue<Runnable> tasks = new SynchronousQueue<>();
    LinkedBlockingQueue<Runnable> buffTasks = new LinkedBlockingQueue<>();

    ThreadPoolExecutor threadPoolExecutor;

    public ElasticityThreadPool(int  , int maximumPoolSize, int keepAliveTime) {
        threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, tasks);
        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

        Thread managerThread = new Thread(() -> {
            while (true) {
                try {
                    Runnable buffTask = buffTasks.take();
                    try {
                        threadPoolExecutor.execute(buffTask);
                    } catch (RejectedExecutionException e) {
                        buffTasks.offer(buffTask);
                    }
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
        managerThread.setName("ElasticityThreadPool-TaskManager");
        managerThread.start();
    }

    @Override
    public void execute(Runnable runnable) {
        buffTasks.offer(runnable);
    }
}

這個線程池使用了有界隊列,而且是一個極端有界隊列 SynchronousQueue , 當 corePoolSize 數量個線程全都忙碌時,新的線程將會被創建。而當 maximumPoolSize 個線程也無法滿足任務的處理時, 飽和策略將會發揮作用

threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

線程池會拒絕執行任務,並拋出 RejectedExecutionException 異常。

我們從代碼中看到,還存在一個名叫 buffTasks 的誤解隊列, 此隊列的用作任務的中轉, 所有提交給線程池的任務事先都會被置入此隊列

public void execute(Runnable runnable) {
  buffTasks.offer(runnable);
}

而管理線程則不斷的從此隊列中讀取任務,並提交給真正的線程池

Thread managerThread = new Thread(() -> {
  while (true) {
    try {
      Runnable buffTask = buffTasks.take();
      try {
        threadPoolExecutor.execute(buffTask);
      } catch (RejectedExecutionException e) {
        buffTasks.offer(buffTask);
      }
    } catch (InterruptedException e) {
      break;
    }
  }
});
managerThread.setName("ElasticityThreadPool-TaskManager");
managerThread.start();
}

當線程池因爲飽和而無法處理的任務通過飽和策略退回是,管理線程會再次將之存入 buffTasks, 以待後續處理

try {
    threadPoolExecutor.execute(buffTask);
} catch (RejectedExecutionException e) {
    buffTasks.offer(buffTask);
}

這裏因爲沒有設置

allowCoreThreadTimeOut

所以被回收的線程按照線程池默認的機制處理,線程池總會保持 corePoolSize 個線程的隨時就緒

線程池使用方法如下

ElasticityThreadPool threadPool = new ElasticityThreadPool(2, 10, 60);

int count = 0;
while (count < 200) {
  threadPool.execute(() -> {
    try {
      Thread.sleep(500);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("hello world");
  });
  count++;
}

這種方法的好處是實現了真正的可伸縮,壞處是多使用了一個額外的線程

兩種方法的孰優孰劣看具體的使用場景而定。

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