Java并发: 我所经历过的坑

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()

===持续更新,也欢迎提供案例===

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值