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,因為會有機會造成任務延遲,導致實際執行時間產生誤差。
沒有留言:
張貼留言