從Java線程池實現原理到如何配置一個合適的線程池

前言

本文會出現一些阻塞隊列相關的知識,這種隊列除了有隊列Queue的特性之外,還有阻塞的特性,例如take方法會獲取元素時若隊列爲空會阻塞直到有元素被添加等等,還有很多特性,可以看這篇文章,帶你瞭解幾個基本的阻塞隊列
除了上面的文章,我還強烈推薦你讀這篇文章,瞭解JDK 1.7 新增的一個高吞吐的隊列實現

本篇文章的議題如下:

  • 線程池的運行原理的源碼分析
  • 如何配置一個合適的線程池?

爲什麼要使用線程池

首先,爲什麼我們需要線程池呢?直接 new Thread().start() 不行嗎?

在 Thread#start 方法執行後,會執行JNI方法,其在底層創建了一個POSIX Thread,然後會在該線程內回調執行我們自定義的Thread#run方法,在執行完run方法或執行期間拋出異常,都會導致該線程的執行結束,接着就會delete自己,釋放POSIX Thread線程資源。

可以看到,在 new Thread().start() 的過程中,有兩個步驟和我們執行具體任務是無關的:

  • 向os申請線程資源,創建POSIX Thread
  • delete自己,釋放POSIX Thread線程資源

在有多個任務需要被多線程執行的時候,以上兩步會在每一個任務執行和結束時重複執行,這兩步其實可以省掉的,這也就是線程池需要做的事情。

所以,線程池第一個好處即爲 減少了創建和釋放線程的開銷

其次,我們使用線程的場景有多種多樣,有時候是IO密集型(IO操作佔比較大的比重,這裏的IO操作指磁盤IO、數據庫IO、網絡IO、大內存操作等等的CPU需要長時間等待的IO操作),有時候是CPU密集型,又有時候方法會被高併發訪問,並且方法爲耗時操作,亦或是低併發,耗時、高併發,短時、低併發、短時等等,場景有很多,單純的new一個線程的操作已經不能很好的幫我們去管理線程。我們需要線程池來決定應該創建多少個線程來做,需要保持多少個線程重複工作才最合適。

所以,線程池的第二個好處即爲 幫助管理和控制線程的數量、行爲

最後,在調試的時候(例如jstack)排查多線程問題,免不了需要查看線程名稱,此時線程池可以給一類工作、場景命名,比如A功能爲一個名字,B功能爲另一個名字,這樣排查起來就可以很好的定位是哪個功能模塊的線程有問題。

所以,線程池的第三個好處即爲 方便調試,定位問題

當然,線程池還有很多好處,例如可以處理線程拋出的異常之類,這裏就不一一贅述,在讀者研究完線程池的源碼,自己使用過線程池之後相信都會體會到。

線程池的運行原理的源碼分析

JDK的線程池的標準實現一般有3種:

  • java.util.concurrent.ForkJoinPool (”竊取“任務,一定場景下高吞吐量)
  • java.util.concurrent.ScheduledThreadPoolExecutor (定時任務,異步的線程定時執行)
  • java.util.concurrent.ThreadPoolExecutor (普通的線程池)

在這裏,我們分析最後一種線程池實現,其他的線程池都是基於同一個思想,所以我們取最common的實現來分析是最有價值的。

線程池狀態

// 表示線程池當前的狀態,使用32位長的int來保存狀態信息
// 其中高3位表示線程池的狀態,低29位表示線程池的數量
// 這裏將ctl控制位初始化爲RUNNING狀態,並且0個線程數量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
// 後29位全爲1,代表線程池線程的最大數量,一般不會創建這麼多
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS; // 32位中高3位爲111
private static final int SHUTDOWN   =  0 << COUNT_BITS; // 32位中高3位爲000
private static final int STOP       =  1 << COUNT_BITS; // 32位中高3位爲001
private static final int TIDYING    =  2 << COUNT_BITS; // 32位中高3位爲010
private static final int TERMINATED =  3 << COUNT_BITS; // 32位中高3位爲011

下面幾個方法來辨別當前的狀態信息:

// Packing and unpacking ctl
// ~表示取反操作,這裏~CAPACITY就代表高3位爲1,低29位爲0的一個數字
// 所以這裏就只是判斷c這個數的高3位有哪些爲1,不關心低29位
// 返回的值就可以與上面5個狀態做比較,所以這個方法是判斷線程池的運行狀態
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 同理可得,這個方法可以判斷c的低29位數量,判斷線程池當前線程數量
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 從上面初始化ctl控制位的ctlOf(RUNNING, 0)使用就可以看出其作用了
private static int ctlOf(int rs, int wc) { return rs | wc; }

啓動線程池

我們知道,調用線程池的execute、submit方法都可以提交一個任務,線程池就會調度線程去執行其run方法,那麼就以此爲入口方法來看看線程池到底在提交任務之後都做了什麼:

public void execute(Runnable command) {
  if (command == null)
    throw new NullPointerException();
  // 獲取控制位,剛創建線程池的時候ctl控制位爲RUNNING狀態,0個線程數
  int c = ctl.get();
  // workerCountOf獲取此時線程數量,上面有提到
  // 判斷線程數量是否小於核心線程數量
  if (workerCountOf(c) < corePoolSize) {
    // 如果此時線程數量小於核心數量,則addWorker創建線程
    if (addWorker(command, true))
      return;
    // 創建線程失敗了,重新獲取控制位
    c = ctl.get();
  }
  // 判斷線程池是否是Running狀態,如果是則將任務入workQueue這個任務隊列
  if (isRunning(c) && workQueue.offer(command)) {
    // 到這裏的話可以說明任務入隊成功
    int recheck = ctl.get();
    // 線程池如果此時被shutDown,此時需要拒絕任務
    if (! isRunning(recheck) && remove(command))
      reject(command);
    // 需要確認一下線程數量,如果沒有線程則需要新增一個線程
    else if (workerCountOf(recheck) == 0)
      addWorker(null, false);
  }
  // 如果到這裏,一般說明核心線程滿了且任務入workQueue隊列失敗了
  // 此時繼續創建一個線程
  else if (!addWorker(command, false))
    // 創建線程失敗了,只能拒絕了
    reject(command);
}

這裏的大致脈絡還算比較清晰,只不過我們需要深入以下3個細節分支:

  1. addWorker:創建線程過程
  2. workQueue:此隊列有什麼用?爲什麼往裏面塞任務就可以執行?
  3. reject:拒絕策略

我們按順序依次分析他們的作用

創建線程

這裏直接來看addWorker方法,是如何創建新的線程的

private boolean addWorker(Runnable firstTask, boolean core) {
  retry:
  for (;;) {
    // 獲取此時控制位
    int c = ctl.get();
    // 獲取運行狀態(只取高3位的信息)
    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);
      // core代表此時是否創建核心線程,false代表創建臨時線程
      // 所以這裏在判斷是否超過最大核心線程數或是否超過最大臨時線程數
      if (wc >= CAPACITY ||
          wc >= (core ? corePoolSize : maximumPoolSize))
        // 超過則創建失敗
        return false;
      // 到這裏則數量夠創建,則將ctl控制位增加1,代表增加一個線程數量
      if (compareAndIncrementWorkerCount(c))
        // 跳出到最外層的retry語句
        break retry;
      // 如果到這裏,說明上面ctl控制位增1衝突了,則需要重新獲取最新的ctl控制位
      c = ctl.get();  // Re-read ctl
      // 判斷此時狀態,若狀態變化了,則跳到最外層retry重新循環
      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 {
    // 到這裏說明ctl增1成功了,所以直接來創建工作線程了
    // 在Worker的構造函數中會創建一個Thread
    w = new Worker(firstTask);
    // 此thread爲構造函數中創建出來的
    final Thread t = w.thread;
    if (t != null) {
      final ReentrantLock mainLock = this.mainLock;
      mainLock.lock();
      try {
        // Recheck while holding lock.
        // Back out on ThreadFactory failure or if
        // shut down before lock acquired.
        int rs = runStateOf(ctl.get());

        // 若狀態爲Running
        if (rs < SHUTDOWN ||
            (rs == SHUTDOWN && firstTask == null)) {
          if (t.isAlive()) // precheck that t is startable
            throw new IllegalThreadStateException();
          // 在workers這個數據結構中新增記錄這個worker
          workers.add(w);
          int s = workers.size();
          if (s > largestPoolSize)
            largestPoolSize = s;
          // 標示添加worker成功了
          workerAdded = true;
        }
      } finally {
        mainLock.unlock();
      }
      // 僅在添加worker成功且成功啓動線程之後,workerStarted才爲true
      // workerStarted爲true才代表最終的添加成功
      if (workerAdded) {
        t.start();
        workerStarted = true;
      }
    }
  } finally {
    // 如果添加沒有成功,需要刪除此worker的一系列信息
    if (! workerStarted)
      addWorkerFailed(w);
  }
  // 返回最終新增worker的結果
  return workerStarted;
}

這裏按正常流程來說,會經歷以下幾個階段:

  1. 更新ctl控制位信息,數量+1
  2. 創建Worker,並添加到workers這個數據結構以記錄此worker
  3. 線程池狀態確認爲RUNING狀態,則執行t.start()方法,啓動線程

由此可見,addWorker的作用就是創建Worker類保存下來,並啓動一個新的線程

線程池的工人 “Worker”

值得一提到是剛剛代碼裏的t.start()方法,其開啓了worker創建的thread,在介紹之前,先來看看Worker類的結構在這裏插入圖片描述
可以看到,Worker其實是一個Runnable,其也使用了AQS的特性(關於AQS,可以看這篇文章

我們先來回顧一下Worker的構造函數是如何創建線程的

Worker(Runnable firstTask) {
  setState(-1); // inhibit interrupts until runWorker
  this.firstTask = firstTask;
  // 獲取線程池的ThreadFactory,傳入Worker對象,創建線程
  this.thread = getThreadFactory().newThread(this);
}

關於ThreadFactory,其在創建線程池時可以配置一個,所以由此可見這個是一個入口,我們可以自定義線程池創建的每一個線程的信息,值得一提的是這裏傳入了this

@Override
public Thread newThread(Runnable r) {
  return new Thread(r, "customThreadName");
}

這裏簡略的給出了一個newThread的簡單實現,這裏將Worker作爲一個Runnable傳入此方法,也就是說,Worker類的成員變量thread線程,其實就是它自己。

啓動工人,開啓線程

所以上面的addWorker方法中的t.start()方法其實就是啓動一個線程,然後此線程回調了自己的run方法(因爲Worker就是一個Runnable),所以到這裏我們需要看Worker的run方法

public void run() {
  runWorker(this);
}

final void runWorker(Worker w) {
  Thread wt = Thread.currentThread();
  // 一般來說,這裏第一個任務就是創建worker時攜帶的需要執行的任務
  Runnable task = w.firstTask;
  // 清空
  w.firstTask = null;
  // worker其實也充當了鎖的角色,這從它AQS的特性可以看出來
  // 至於爲什麼這裏要鎖,後面會詳細講解
  w.unlock(); // allow interrupts
  boolean completedAbruptly = true;
  try {
    // 第一次task不爲空,直接執行任務
    // 後面的話都會去getTask方法裏取任務
    // 換句話說,在while循環中,task這個局部變量都是不爲null的,都是有任務的
    while (task != null || (task = getTask()) != null) {
      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方法處理
          // 此方法爲空方法,留給用戶自定義出現異常時的操作或是沒異常的完成任務操作
          afterExecute(task, thrown);
        }
      } finally {
        task = null;
        w.completedTasks++;
        w.unlock();
      }
    }
    completedAbruptly = false;
  } finally {
    processWorkerExit(w, completedAbruptly);
  }
}

分析到這裏我們可以知道,addWorker創建並啓動的這個線程,其實就是在一直跑,也就是runWorker方法中不停的做while循環,直到循環條件 getTask() 爲空纔會停下。

工人獲取任務

工人除了去執行第一個創建時順便攜帶的任務之外,還會一直去獲取”上級“分配下來的任務,這裏的上級就是workQueue這個阻塞隊列。這裏我們重點看看 getTask() 方法

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.
    // 如果線程池正在被shutdown,就不取任務了,返回null停止worker的無限循環
    if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
      decrementWorkerCount();
      return null;
    }

    // 獲取線程數量
    int wc = workerCountOf(c);

    // Are workers subject to culling?
    // allowCoreThreadTimeOut這個參數在創建線程池時可以配置爲true
    // 表示核心線程也視爲臨時worker,也會過期釋放資源
    // 或者線程數量已經大於核心數量,則現在的worker都是臨時工,過一段空閒時間會被清除
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

    // 當timed = true 表示worker爲臨時工
    // 且timedOut = true(在下面被設置) 表示已經超過了一段空閒時間
    // 且workQueue爲空表示現在沒有任務,線程池空閒狀態 或wc大於1 表示留一個線程也夠
    if ((wc > maximumPoolSize || (timed && timedOut))
        && (wc > 1 || workQueue.isEmpty())) {
      // 滿足以上條件,都會開始清理worker,表示當前線程池開始進入空閒狀態了
      // 減少線程數
      if (compareAndDecrementWorkerCount(c))
        // 返回空,讓worker線程從while循環中break出來
        return null;
      continue;
    }

    try {
      // timed表示此時是否爲臨時工
      // 這裏可以看出,如果是臨時工,會從任務隊列workQueue中有時限地拿任務
      // 而不是臨時工的話,將一直阻塞,直到workQueue中有任務
      Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
      workQueue.take();
      if (r != null)
        // 獲取到任務,返回出去執行
        return r;
      // 到這裏的話,就是沒獲取到任務,就表示線程池有點空閒了,也只有臨時工纔會到這一步
      // 則設置timedOut=true,準備開始清理臨時工
      timedOut = true;
    } catch (InterruptedException retry) {
      timedOut = false;
    }
  }
}

這裏的邏輯就會分爲正式員工和臨時工了,我們知道,正式員工與公司是有合同的,不會輕易辭退,而臨時工則是在公司比較忙的時候臨時添加的人手,在公司不忙的時候其實臨時工就沒有多大存在的必要了。以此類推也可以看出以上代碼也是符合這個邏輯的,正式工人會一直阻塞去取workQueue裏的任務,而臨時工只會阻塞一會時間,這個時間是我們創建線程池時可以自定義的一個空閒時間,在超過這個空閒時間後還沒任務,那些臨時工人就會消亡(本質是worker線程的run方法執行結束,底層JVM會delete釋放os線程,讀者只需要知道,線程的run方法結束就會釋放線程資源)。

拒絕策略

在最開頭中,線程池的execute方法我們可以看到,在線程大於核心線程數且隊列也塞不下任務且增加臨時工也失敗(超過最大線程閾值,自行配置的值)的情況下,就會拒絕此任務,執行reject方法

final void reject(Runnable command) {
    handler.rejectedExecution(command, this);
}

可以看到,這裏拒絕的邏輯交給了handler類的rejectedExecution方法去做,而handler是在哪裏被初始化的呢?我們全局搜索可以看到,其在線程池的構造函數中被初始化

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

也就是說,我們可以自定義拒絕策略,這裏我們看看JDK自帶的幾個默認實現

默認拒絕策略(拋異常)

public static class AbortPolicy implements RejectedExecutionHandler {
    public AbortPolicy() { }
  
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

如果不配置拒絕策略,默認在任務滿的時候會拋出一個異常

最舊淘汰策略(丟棄)

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public DiscardOldestPolicy() { }
  
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}

再介紹一個前段時間剛用的策略,場景是定時更新某個數據,若任務滿了,可以將最舊的任務從隊列中丟棄,然後執行我這個任務,因爲最舊的更新任務可能已經過了某個時效,可以允許被丟棄

讀者還可以查看RejectedExecutionHandler的子類,查看更多JDK自帶實現,從而吸收其思想,相信自定義一個符合業務場景的拒絕策略實現並不是一件難事

關閉線程池

在需要釋放線程池中的線程資源時我們通常會調用以下方法關閉線程池,釋放線程資源

  • shutdown:讓線程先完成手頭上的工作,再設置interrupt中斷標識,結束時機交給工人決定
  • shutdownNow:直接設置interrupt中斷標識,結束時機依然是工人決定

這兩個方法的區別僅僅是設置interrupt的時機,而真正的結束由Worker去判斷interrupt標識,下面直接來看代碼看看兩個方法是如何實現的

溫和關閉線程池

首先看看shutdown方法,其關閉過程還是比較溫柔的

public void shutdown() {
  final ReentrantLock mainLock = this.mainLock;	
  mainLock.lock();
  try {
    checkShutdownAccess();
    // 設置線程池狀態爲SHUTDOWN
    advanceRunState(SHUTDOWN);
    // 僅中斷Idle(空閒)的工人
    interruptIdleWorkers();
    // 線程池關閉鉤子(hook),空方法
    // 其在ScheduledThreadPoolExecutor線程池中有實現
    onShutdown(); // hook for ScheduledThreadPoolExecutor
  } finally {
    mainLock.unlock();
  }
  tryTerminate();
}

關鍵在interruptIdleWorkers方法,是如何判斷哪些工人是空閒的

private void interruptIdleWorkers() {
  interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    // 遍歷工人集合
    for (Worker w : workers) {
      Thread t = w.thread;
      // 如果工人還沒有被中斷,此時就來獲取工人鎖
      if (!t.isInterrupted() && w.tryLock()) {
        try {
          // 能到這裏,表示工人此時是空閒的
          // 因爲工人如果在忙,工人鎖是會被工人獨佔的
          // 中斷工人
          t.interrupt();
        } catch (SecurityException ignore) {
        } finally {
          w.unlock();
        }
      }
      if (onlyOne)
        break;
    }
  } finally {
    mainLock.unlock();
  }
}

我們可以回憶一下,在創建Worker並啓動線程中,會啓動worker線程的run方法,其本質是在一段while循環中不斷獲取任務並執行,在while循環中,若獲取到了任務,則會將自身作爲鎖鎖住,表示正在執行任務,若此時shutdown方法被調用,則獲取此“忙工人”這把鎖會被阻塞住,直到工人執行完這個任務,釋放了工人鎖纔會被設置中斷標識,等全部工人都被設置了中斷標識後線程池就真正被關閉了。

暴力關閉線程池

說是暴力,其實也不會特別的暴力,話不多說,直接進入shutdownNow方法

public List<Runnable> shutdownNow() {
  List<Runnable> tasks;
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    checkShutdownAccess();
    // 直接設置爲STOP
    advanceRunState(STOP);
    // 直接中斷工人
    interruptWorkers();
    // 將沒執行的任務返回出去
    tasks = drainQueue();
  } finally {
    mainLock.unlock();
  }
  tryTerminate();
  return tasks;
}

和上面不同的,這裏會將還未執行的任務返回出去,並且中斷是直接了當的

private void interruptWorkers() {
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    for (Worker w : workers)
      // 直接中斷,並不獲取鎖
      w.interruptIfStarted();
  } finally {
    mainLock.unlock();
  }
}

但歸根結底,何時中斷返回還是由工作線程決定的,若工作線程中沒有判斷中斷標識的地方,則也不會立馬返回,還是會執行完手頭上的工作纔會返回,若線程執行的方法中有阻塞線程的方法(大部分的阻塞方法都會被interrupt方法喚醒且判斷標識,拋出一箇中斷異常),則此時會拋出異常,強行結束。

若線程中存在阻塞方法並且忽略中斷異常,則此時線程永遠也關閉不了!

亦或是線程中有無限阻塞方法,若只調用shutdown是關閉不了的!

綜上所述,任務需要注意以下兩點:

  1. 不能忽略中斷異常!需要被合理的處理
  2. 儘量避免無限阻塞方法(大概率也不會出現),若有這個可能,需要有意識轉爲調用shutdownNow來關閉線程池

小結

到這裏,線程池的流轉就大致結束了,我這裏爲了總結線程池的工作原理,畫了下面這張圖,希望讀者也可以跟着思路,也畫出這麼一張流轉圖,相信會更好的理解整個過程
在這裏插入圖片描述

配置一個合適的線程池

關於如何配置一個合適的線程池,其實這個話題很大很廣,視不同業務,不同場景,不同機器配置而定,所以這裏沒有通用公式,只是介紹幾個場景,希望讀者可以觸類旁通。

合適的線程數量

一般來說:

  • CPU密集型:CPU總是會需要等待IO,假設CPU滿載(CPU利用率100%,最理想情況),則此時爲CPU密集型,可配置線程池數量爲:Core核心數 + 1,可以在每個CPU核上各自跑一個線程,理想情況下沒有上下文切換開銷

  • IO密集型:若一個任務的執行有很多的時間都在等待IO操作(例如數據庫的IO、磁盤的IO、網絡IO等等),此時稱之爲IO密集型。在執行IO操作時系統會將線程暫時休眠,切換別的線程繼續執行任務,等IO操作完之後纔會切換回來原線程繼續執行,所以爲了最大剝削CPU能力,在每個核上的每個時間都在跑線程,我們需要配置線程數量(當一半時間都在等待時)爲:(Core核心數 * 2 )+ 1 ,這是由於等待時間佔一半時的線程數量,並不是IO密集型都是這個數量,通用的,如果IO操作(會阻塞你線程的操作)比較多,你需要用以下公式計算合適的線程數:

    任務執行時間 /(任務執行時間-IO等待時間)✖️ CPU內核數

    例如:一個任務需要100ms執行完成,其中IO的時間需要花去50ms來等待,則:

    100 / (100 - 50) = 2,則最終需要兩倍的CPU核心數

    若業務中還需要等待網絡IO之類的不定的等待時間,有時候是50,有時候100,你可以取一個平均數

以上線程數是一個大致的估測值,並不能說是最終最合適的,你需要在這之後進行測試,然後再適當調整,一般來說在複雜場景下需要不斷微調纔可以知道大致的合適線程數量

合適的阻塞隊列

線程池配置怎麼和阻塞隊列還有關係?不同的阻塞隊列都有不同的特點,其決定了線程池中任務的存取,也是比較關鍵的一個配置項,下面介紹幾個常用的阻塞隊列

  • 在JDK 1.7 更新了一個新的隊列實現 LinkedTransferQueue ,具體教程可以看這裏,其是以上幾個隊列的超集,又兼備ConcurrentLinkedQueue的吞吐量,不過此隊列是無界的,而hand-off特性由新的api提供,如果線程池需要用到此隊列的hand-off特性,需要自己另外封裝api,建議讀者可以瞭解並學習,將此隊列列爲首選
  • SynchronousQueue:傳球手,本身不存數據,若沒有worker在take任務(表示沒有空閒的線程),線程池在接受一個任務時會直接創建worker線程(因爲其offer方法沒有接收方,此方法不阻塞直接返回),這個阻塞隊列的吞吐量是很高的,缺點就在會一直創建線程,在接受任務的時候(若設置了max,則會一直拒絕,不太合適)
  • LinkedBlockingQueue:構造函數中若有傳值則爲有界隊列,使用默認構造器則爲無界隊列,其可以存放任務元素,起到任務緩衝的作用,一般與ArrayBlockingQueue進行比較,其不需要分配一段連續內存,而是用鏈表形式保存一個鏈表引用,所以在不確定任務數量的情況下可以使用,沒有初始化一大段內存的開銷。也是很多一般線程池的選擇
  • ArrayBlockingQueue:有界隊列,在初始化之後會分配一段額定的連續內存,其缺點就在剛開始就需要分配一段數組內存,但優點在於連續,可以很好的利用CPU緩存特性load一段連續內存作爲緩存,在任務比較確定其吞吐量時可以使用,一般比較穩定

低耗時任務

這裏不論高併發,還是低併發,都可以一併來講

若使用SynchronousQueue或LinkedTransferQueue 吞吐量優先的隊列,一般不太設置核心線程,隨着併發量讓線程數保持一個配合併發量的合適的值
但要注意,使用LinkedTransferQueue的hand-off特性需要自己封裝api,因爲其handoff的api改變了

如果一個接口任務耗時比較低,那此時隊列的吞吐量就顯得比較重要了,所以會偏向使用SynchronousQueue或LinkedTransferQueue隊列

如果併發量比較高,網關是需要控制併發量,做好適當的限流的,不然這個可怕的隊列會讓你的線程池無限創建線程,將會造成內存溢出的異常,導致系統崩潰。最好在做好限流的同時設置一個max線程數量,以防線程創建過多

高耗時任務

這裏需要考慮線程數量的配置

此時任務耗時比較高,需要知道是因爲CPU計算量大而導致的高耗時,還是IO或其他阻塞動作導致的高耗時,如果是後者,需要使用ArrayBlockingQueue或是LinkedBlockingQueue這類的隊列(熟悉的話首選LinkedTransferQueue,缺點是無界),然後合適的確定一個線程數量配置在線程池中,讓線程池和隊列很好管理和控制線程數量,做到最合適的吞吐。如果是前者,可能需要考慮算法的複雜度和CPU的核心數量的增加了,不然也頂不住這高併發…

定時任務

線程數量:如果是定時任務,並不考慮即時性,配置1-2個線程都是可以的

回收線程資源:若場景以天、周等等大時間爲單位,需要將線程池中的線程都設置爲臨時工,不然會一直佔用系統內存等等資源,其只是定期跑一跑而已,沒有必要一直佔用資源。可以調用線程池的allowCoreThreadTimeOut方法,將所有線程都設置爲臨時工

當然了,以上結論都只是一個參考,給讀者提供的一些思路,若死板的應用在項目中,是不會輕易達到最適合的要求的。這需要讀者瞭解線程池運行原理,多配置幾次積攢一些經驗纔可以更好的配置一個比較合適的線程池。

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