1. Runnable和Callable不要抛异常
Runnable.run和Callable.call方法都不应该抛出异常,JDK内很多组件都没有处理这些方法抛出的异常,比如定时执行任务的Timer、ThreadPoolExecutor的Worker。
1. Timer
我们先来看一个Timer的例子,假设我们有两个任务,HeartBeat表示心跳,每1s在打印一条信息;Broken则抛出异常。下面是这两个类的代码
public static class HeartBeat extends TimerTask {
public void run() {
println("heart beat...");
}
}
public static class Broken extends TimerTask {
public void run() {
println("start broken runner");
throw new RuntimeException("throw exception");
}
}
现在我们来写调度代码,让HeartBeat先启动,这样我们能观察到Timer是在正常运行的。Broken在2min后再启动
Timer timer = new Timer("timer-thread-");
timer.schedule(new HeartBeat(), 0, 1000);
timer.schedule(new Broken(), 2*60*1000); // 延迟2分钟,分别打jstack
sleep(10*60*1000); // 休眠10分钟,方便做jstack看看timer的线程还在不在
可以看到Broken运行之前,有一个timer-thread-线程,但是Broken运行后,这个线程就丢失了,连带着Timer运行异常,HeartBeat不再打印
2. ScheduledThreadPoolExecutor
上面同样的代码,如果交由ScheduledThreadPoolExecutor来执行的话,整个HeartBeat是一直运行正常的。我们先给出用ScheduledThreadPoolExecutor实现的代码
public static class ScheduledThreadFactory implements ThreadFactory {
private AtomicInteger seq = new AtomicInteger(0);
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("timer-thread-" + seq.incrementAndGet());
return t;
}
}
public static class HeartBeat implements Runnable {
public void run() {
println("heart beat...");
}
}
public static class Broken implements Runnable {
public void run() {
println("start broken runner");
sleep(60_000);
throw new RuntimeException("throw exception");
}
}
public static void main(String[] args) {
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(3, new ScheduledThreadFactory());
scheduled.scheduleAtFixedRate(new HeartBeat(), 0, 1, TimeUnit.SECONDS);
scheduled.schedule(new Broken(), 30, TimeUnit.SECONDS);
sleep(10 * 60 * 1000);
}
SchduledThreadPoolExecutor会将提交的Runnable封装为ScheduledFutureTask,而ScheduledFutureTask继承自FutureTask,FutureTask中对任务执行异常的情况做了封装
2. 不共享非线程安全的类实例
JDK内部提供的一些类是非线程安全的,这些类包括SimpleDateFormat、HashMap等类都不是线程安全的,如果在多线程环境使用各有各的问题。比如SimpleDateFormat在多线程环境使用可能会抛出NumberFormtaException异常,HashMap多线程环境使用可能会导致迭代时死循环。
下面这段代码用来复现SimpleDateFormat在多线程中使用遇到的问题,抛出NumberFormatException
public static void main(String[] args) throws InterruptedException {
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
try {
simpleDateFormat.parse("2024-05-21");
} catch (ParseException e) {
throw new RuntimeException(e);
}
});
}
executorService.shutdown();
}
以SimpleDateFormat为例,解决方案有3种,1: 将SimpleDateFormat作为局部变量;2: 使用ThreadLocal持有; 3: 寻找线程安全的替代类。
3. 无限阻塞的FutureTask
当我们通过ExecutorService提交任务时,ExecutorService会将任务包装为FutureTask,线程池执行时会在运行结束后调用FutureTask.set、FutureTask.setException来标记FutureTask执行完成。然而在我们使用特定的RejectedExecutionHandler时,会绕过将FutureTask设置为完成状态的代码。这个时候,如果我们还调用Future.get(),那么线程将一直阻塞。
我们用下面这段代码模拟了这种场景,首先我们定义了一个LongWait类,目的是让它占用线程池的线程,好让线程池没有线程可用,走RejectedExecutionHandler的逻辑
public static class LongWait implements Runnable {
public void run() {
System.out.println("long wait start sleep...");
try {
Thread.sleep(30_000);
} catch (InterruptedException e) {
}
System.out.println("long wait after sleep...");
}
}
我们自定义一个RejectedExecutionHandler,实现和DiscardPolicy类似,不多我们加了个打印,为了让我们能感知任务被reject
public static class MyDiscard implements RejectedExecutionHandler {
public MyDiscard() {
}
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
System.out.println("discard " + r);
}
}
下面是调度代码,通过日志能看到的是future.get()一直没有返回,最后的一个代码无法输出,具体看下面的线程堆栈信息
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor es = new ThreadPoolExecutor(1, 1, 1, TimeUnit.MINUTES, new SynchronousQueue<>(), new MyDiscard());
es.submit(new LongWait());
System.out.println("poolSize:" + es.getPoolSize());
Future<?> future = es.submit(new LongWait());
System.out.println("poolSize:" + es.getPoolSize());
Object o = future.get();
System.out.println("after future.get: " + o);
}
这个是阻塞时的异常堆栈信息,可以看到main方法一直阻塞在future.get()
===持续更新,也欢迎提供案例===