2014年2月19日 星期三

ExecutorService 的 Thread Pool 效果

這是一篇簡易的測試文,目的是測試 Java 的 ExecutorService 中宣稱的 Thread Pool 的效果。

Thread 的產生時機與 Thread Pool 測試

首先,ExecutorService 創建時,可以指定 Thread Pool 的容量以及對應的 ThreadFactory
在 ThreadFactory 中可以定義如何產生 Thread,因此可以利用 ThreadFactory 來檢驗 Thread Pool 中的 Thread 的產生時機。

程式碼中分成兩個 .java 檔,一個是初始化 ExecutorService 並提供介面做排程的類別(Schedule.java),一個則是程式進入點(Test.java),進行排程設定的動作。

Schedule.java
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

public class Schedule {

  private static ScheduledExecutorService SCHEDULER = null;
  private static ThreadGroup handlingGroup = new ThreadGroup("ThreadGroup");;
  private static Logger log = Logger.getLogger("Scheduler");
  
  public static void initial () {
    // Initiate a ExecutorService with pre-defined thread pool.
    SCHEDULER = Executors.newScheduledThreadPool(2, new ScheduleHandlerThreadFactory("Threads"));
  }
  
  public static void schedule (Runnable command, long delay, TimeUnit timeUnit) {
    SCHEDULER.schedule(command, delay, timeUnit);
  }
  
  private static class ScheduleHandlerThreadFactory implements ThreadFactory {
    private int count = 1;
    
    private String prefixString = null;
    
    public ScheduleHandlerThreadFactory (String prefix) {
      this.prefixString = prefix;
    }
    
    @Override
    public Thread newThread(Runnable r) {
      log.debug("Create thread #" + this.count);
      return new Thread(handlingGroup, r, this.prefixString + "-" + this.count++);
    }
  }
}

在 Schedule.java 中,產生的 ExecutorService 是有兩個 Thread 的 Thread Pool,同時在產生 Thread 時會在 log 上印出現在產生的序號。

Test.java
import java.util.concurrent.TimeUnit;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

public class Test {
  private static Logger LOG = LogManager.getLogger("Main");
  
  public static void main(String[] args) {
    LOG.debug("Initial scheduler.");
    Schedule.initial();
    
    LOG.debug("Sleep.");
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {}
    
    LOG.debug("Set schedule #1.");
    Schedule.schedule(new TestRunnable(), 0, TimeUnit.MILLISECONDS);
    LOG.debug("Set schedule #2.");
    Schedule.schedule(new TestRunnable(), 1000, TimeUnit.MILLISECONDS);
    LOG.debug("Set schedule #3.");
    Schedule.schedule(new TestRunnable(), 1000, TimeUnit.MILLISECONDS);
    LOG.debug("Set schedule #4.");
    Schedule.schedule(new TestRunnable(), 1000, TimeUnit.MILLISECONDS);
    
    LOG.debug("Sleep again.");
    try {
      Thread.sleep(5000);
    } catch (InterruptedException e) {}
    LOG.debug("End");
    System.exit(0);
  }
  
  public static class TestRunnable implements Runnable {

    @Override
    public void run() {
      LogManager.getLogger(Thread.currentThread().getName()).debug("RUNNING by thread " + Thread.currentThread().getId());
    }
    
  }
}

在 Test.java 中,依序設定 4 個排程,其中第 2~4 個排程都是延後 1 秒才執行,以此來判斷 Thread 產生的時機。
最後印出的結果如下:

2014-02-19 12:03:14.637 | DEBUG | Main > Initial scheduler.
2014-02-19 12:03:14.644 | DEBUG | Main > Sleep.
2014-02-19 12:03:16.644 | DEBUG | Main > Set schedule #1.
2014-02-19 12:03:16.647 | DEBUG | Scheduler > Create thread #1
2014-02-19 12:03:16.649 | DEBUG | Main > Set schedule #2.
2014-02-19 12:03:16.649 | DEBUG | Scheduler > Create thread #2
2014-02-19 12:03:16.649 | DEBUG | Threads-1 > RUNNING by thread 12
2014-02-19 12:03:16.649 | DEBUG | Main > Set schedule #3.
2014-02-19 12:03:16.651 | DEBUG | Main > Set schedule #4.
2014-02-19 12:03:16.651 | DEBUG | Main > Sleep again.
2014-02-19 12:03:17.650 | DEBUG | Threads-2 > RUNNING by thread 13
2014-02-19 12:03:17.652 | DEBUG | Threads-1 > RUNNING by thread 12
2014-02-19 12:03:17.652 | DEBUG | Threads-2 > RUNNING by thread 13
2014-02-19 12:03:21.652 | DEBUG | Main > End

從印出的結果來看,可以看出 Thread Pool 中的第一個 Thread 是在第 4 行中第一個排程設定時被產生出來的,不是第 2 行就產生出來
所以 ExecutorService 的 Thread 並不會在宣告時就立即產生對應數量的 Thread Pool,而是在開始需要這個 Thread 的時候才產生。
而第二個 Thread 產生的時機也不是跟著第一個 Thread 之後馬上產生出來,而是第二個排程設定進去時才產生
除了驗證了上面的說法之外,同時也表示 Thread 的產生時機是在排程設定的時候,而不是排程真正開始執行的時候。

接著在每個排程執行時,Thread 的編號都是 12、13,也表示 ExecutorService 的確有讓大家共用同一個 Thread Pool~。

定時排程的時機測試

為了想了解 ExecutorService 在做定時排程時,Thread Pool 跟排程時間之間的關係,接著用以下的程式碼做了第二步測試。
程式碼中主要是把 Thread Pool 改成只有一個 Thread 的 Thread Pool
另外產生兩個一樣的任務,一個是主程式一開始就馬上執行、另一個會延遲兩秒才開始執行,任務的定時設定為每一分鐘執行一次
任務的 Runnable 改為印出開始執行的訊息後(訊息中夾帶當前時間)就暫停 10 秒。

程式碼如下:
Test.java
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

public class Test {
  private static Logger LOG = LogManager.getLogger("Main");
  
  public static void main(String[] args) {
    LOG.debug("Initial scheduler.");
    Schedule.initial();
    
    LOG.debug("Sleep.");
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {}
    
    LOG.debug("Set schedule #1.");
    Schedule.scheduleWithFixedDelay(new TestRunnable(1), 0, 60000, TimeUnit.MILLISECONDS);
    LOG.debug("Set schedule #2.");
    Schedule.scheduleWithFixedDelay(new TestRunnable(2), 2000, 60000, TimeUnit.MILLISECONDS);
    
    try {
      Thread.sleep(50000000);
    } catch (InterruptedException e) {}
    LOG.debug("End");
    System.exit(0);
  }
  
  public static class TestRunnable implements Runnable {
    private int _number = 0;
    DateFormat _formatter = new SimpleDateFormat ("hh:mm:ss"); 
    
    public TestRunnable (int count) {
      this._number = count;
    }

    @Override
    public void run() {
      LogManager.getLogger(Thread.currentThread().getName()).debug(
          "Job #" + _number + " is running at " + _formatter.format(Calendar.getInstance().getTime()));
      try {
        Thread.sleep(10000);
      } catch (InterruptedException e) {}
    }
  }
}

Schedule.java
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;

public class Schedule {

 private static ScheduledExecutorService SCHEDULER = null;
 private static ThreadGroup handlingGroup = new ThreadGroup("ThreadGroup");;
 private static Logger log = Logger.getLogger("Scheduler");
 
 public static void initial () {
  // Initiate a ExecutorService with pre-defined thread pool.
  SCHEDULER = Executors.newScheduledThreadPool(1, new ScheduleHandlerThreadFactory("Threads"));
 }
 
 public static void schedule (Runnable command, long delay, TimeUnit timeUnit) {
  SCHEDULER.schedule(command, delay, timeUnit);
 }
 
 public static void scheduleWithFixedDelay (Runnable command, long delay, long period, TimeUnit timeUnit) {
  SCHEDULER.scheduleWithFixedDelay(command, delay, period, timeUnit);
 }
 
 private static class ScheduleHandlerThreadFactory implements ThreadFactory {
  private int count = 1;
  
  private String prefixString = null;
  
  public ScheduleHandlerThreadFactory (String prefix) {
   this.prefixString = prefix;
  }
  
  @Override
  public Thread newThread(Runnable r) {
   log.debug("Create thread #" + this.count);
   return new Thread(handlingGroup, r, this.prefixString + "-" + this.count++);
  }
 }
}

在完全沒有外在因素干擾的狀況下,我的預設結果是認為假設任務 1 在 3:00:00 執行一次,下一次執行應該會在 3:01:00。
而程式的執行結果如下~
2014-02-19 17:01:24.420 | DEBUG | Main > Initial scheduler.
2014-02-19 17:01:24.425 | DEBUG | Main > Sleep.
2014-02-19 17:01:26.425 | DEBUG | Main > Set schedule #1.
2014-02-19 17:01:26.427 | DEBUG | Scheduler > Create thread #1
2014-02-19 17:01:26.427 | DEBUG | Main > Set schedule #2.
2014-02-19 17:01:26.428 | DEBUG | Threads-1 > Job #1 is running at 05:01:26
2014-02-19 17:01:36.428 | DEBUG | Threads-1 > Job #2 is running at 05:01:36
2014-02-19 17:02:36.427 | DEBUG | Threads-1 > Job #1 is running at 05:02:36
2014-02-19 17:02:46.427 | DEBUG | Threads-1 > Job #2 is running at 05:02:46
2014-02-19 17:03:46.426 | DEBUG | Threads-1 > Job #1 is running at 05:03:46
2014-02-19 17:03:56.426 | DEBUG | Threads-1 > Job #2 is running at 05:03:56

從結果可以看出,ExecutorService 的排程其實是依據上次執行結束的時間,加上設定的時間區間來決定下次開始執行的時間
同時就算開始執行的時間到了,也會因為 Thread Pool 滿載的關係而導致開始執行的時間被延後
綜合來說,可以表示 ExecutorService 的任務基本上是無法預測執行的時間的,而且任務越多、距離初始化的時間越久,任務的實際執行時間會跟預期的時間差距越遠。

就上面的結論來看,如果需要的排程是時間要比較精準,那就不適合利用 scheduleWithFixedDelay() 來定時執行
反而應該用每次設定只執行一次的 on-shot 版的函式 schedule() 來觸發執行
同時在 Runnable 執行結束時手動放入下一次 on-shot 執行的排程進去。
而如果一點誤差都沒有,那麼建議是自行產生自己的 ExecutorService,並依照上述的 on-shot 的方式每次執行完,自己幫自己設定下次的執行時間
不要讓排程跟其他任務共用同一個 Thread Pool,因為會有機會造成任務延遲,導致實際執行時間產生誤差。

沒有留言: