前言:
我们日常总会碰到这样的需求:
- 把这个任务放到另外一个线程执行。
- 我需要周期性执行任务的线程。那我们通常都是怎么解决这个问题呢?
- 对于
问题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追踪方便极大的困难。建议项目中的线程都遵从同一个标准。本文仅是个人实践拙见,欢迎拍砖!
架构技术进阶资料
最新面试真题