前言:
我們日常總會碰到這樣的需求:
- 把這個任務放到另外一個線程執行。
- 我需要周期性執行任務的線程。那我們通常都是怎么解決這個問題呢?
- 對于問題1,最簡單的做法就是new thread,然后結束了。但在生產環境中,你常常會困擾,我這個線程到底執行沒,或者是否正在執行。任務報錯了日志在哪里呢?
- 對于問題2,我們可能會選擇定時器,ScheduledExecutor,但如果你想追蹤你的線程運行情況,或者捕獲異常,這都沒法隨心所欲
針對上面常見的兩個問題,我自己封裝了一個標準的后臺線程模型
一、完整代碼實現
/**
* DefaultThread 后臺標準線程
*/
public final class DefaultThread extends Thread {
/** log */
private static final Logger log = InternalLoggerFactory.getLogger(DefaultThread.class);
/** 標準后臺線程循環間隔時間 - 1分鐘 */
public static final long DEFAULT_INTERVAL = 60 * 1000;
/** 標準后臺線程循環最小間隔 - 300ms */
public static final long MIN_INTERVAL = 300;
/** 默認日志輸出級別 -- debug */
public static final LogLevel DEFAULT_LEVEL = LogLevel.DEBUG;
private String name;
private Runnable runnable;
private boolean runOnce;
private volatile boolean stop;
private volatile long interval;
private volatile LogLevel level = DEFAULT_LEVEL;
/**
* 構造函數
* <p>將構造一個標準后臺線程,每分鐘運行1次
* @param name
* @param runnable
*/
public DefaultThread(String name, Runnable runnable) {
super(name);
this.name = name;
this.runnable = runnable;
this.runOnce = false;
this.stop = false;
this.interval = DEFAULT_INTERVAL;
}
/**
* 構造函數
* <p>將構造一個標準后臺線程
* @param name
* @param runnable
* @param runOnce
* @param interval
*/
public DefaultThread(String name, Runnable runnable, boolean runOnce) {
super(name);
this.name = name;
this.runnable = runnable;
this.runOnce = runOnce;
this.stop = false;
this.interval = DEFAULT_INTERVAL;
}
/**
* 構造函數
* <p>將構造一個標準后臺線程,指定間隔運行一次(不能小于最小間隔時間{@link #MIN_INTERVAL}})
* @param name
* @param runnable
* @param runOnce
* @param interval
*/
public DefaultThread(String name, Runnable runnable, long interval) {
super(name);
this.name = name;
this.runnable = runnable;
this.runOnce = false;
this.stop = false;
this.interval = Math.max(MIN_INTERVAL, interval);
}
@Override
public void start() {
super.start();
}
public void stop() {
try {
this.interrupt();
} catch (Exception e) {
// Ignore
}
this.stop = true;
}
/**
* 獲得線程名稱
*/
public String getThreadName() {
return this.name;
}
/**
* 設置日志隔離級別
*/
public void setLogLevel(LogLevel level) {
this.level = level;
}
/**
* 執行任務
* @see JAVA.lang.Thread#run()
*/
public final void run() {
if (runOnce) {
runOnce();
return;
}
while (!stop) {
runOnce();
try {
sleep(interval);
} catch (InterruptedException e) {
// Ignore this Exception
}
}
}
/**
* 執行一次
*/
private void runOnce() {
long startTime = 0;
if (log.isLogEnabled(level)) {
startTime = System.currentTimeMillis();
}
try {
runnable.run();
} catch (Throwable t) {
log.error("thread run error [name:{}, runOnce:{}]", t, name, runOnce);
}
if (log.isLogEnabled(level)) {
log.log(level, "{}#{}", (System.currentTimeMillis() - startTime));
}
}
二、代碼解讀
線程名稱變成強制性
創建標準線程,必須指定線程名稱,這是為了方便在jstack等工具中追蹤
可以定義為周期性執行
周期性執行中做了預防措施,防止定義過小的周期,引起死循環,占用過高CPU
每次執行可追蹤
每次執行,如果在debug模式下將記錄執行時間,對執行異常也進行捕獲打印日志,方便追蹤bug
線程可停止
Java提供的線程并沒有提供停止方法,該封裝中通過一個標志位實現該功能,能夠中斷線程
結語:
線程雖然簡單,但在實際使用中也要注意規范,每個線程都是隨意new,隨意使用的話會造成后續維護,bug追蹤方便極大的困難。建議項目中的線程都遵從同一個標準。本文僅是個人實踐拙見,歡迎拍磚!