Java线程池还能死锁?一篇文章带你搞懂线程池中的一些坑点

科技   2024-12-30 17:02   江苏  

将 脚本之家 设为“星标
第一时间收到文章更新

原创:程序员牛肉(ID:gh_8adabf391378)

最近在线程池这里又踩了一个坑:“线程池死锁”,不知道你们有没有遇到过这种情况。如果你不知道这种情况的话,那可得好好看看我这篇文章了。
我们先来回顾一下什么是死锁:死锁产生的必要条件是互斥、请求与保持、不可抢占、循环等待。所以当若干进程因竞争而无休止地相互等待他方释放已占有的资源时,系统会产生死锁。
简单的讲:如果资源A和B在同一时间只能被单个线程获取,此时线程A获取了资源A,等待线程B释放资源B,而线程B获取了资源B,等待线程A获取线程B。
[最常见的死锁其实就在我们的生活中,当你去一些地方办事的时候,你会遇见以下情况:找A的时候,A说你去找B办。找到B的时候,B说找A办。这个时候AB之间就构成死锁了。]
难道说线程池还会发生这种情况?当然会有,不然我写这篇文章干什么。
线程池引入了另一种死锁情况:父线程在占用了线程池内所有的资源后又向线程池提交了新的任务,并且要等这些任务完成后才释放资源,而这些新提交的任务根本就没机会被完成,一直被堆放在阻塞队列中。
这种情况在项目代码中,大部分都是因为父子线程都使用公共线程所造成的。
例如有一个公共线程池,最大线程数为2。此时有两个线程接收到了任务进行执行。而这个任务需要创建一个子线程来执行。
于是这两个线程又尝试使用公共线程池中的线程来执行任务。结果由于线程池中所有的两个线程都已经被占有,导致没有办法创建子线程来执行任务。
而线程池中的两个线程又因为自身任务没有被执行完毕而一直存活,导致迟迟不肯让渡线程来让子线程执行任务。因此子线程就被一直存放在了有界阻塞队列中。导致后续的所有请求都一直触发线程池的淘汰策略。
示例代码为:
// 创建单线程的线程池
ExecutorService pool = Executors.newSingleThreadExecutor();

    pool.submit(() -> {
    try {
      // 输出日志信息,表示第一个任务开始执行
      log.info("First");

      // 向线程池提交第二个任务,并等待第二个任务执行完成
      pool.submit(() -> log.info("Second")).get();

      // 输出日志信息,表示第一个任务后续操作继续执行
      log.info("Third");
    } catch (InterruptedException | ExecutionException e) {
      // 若出现异常,记录错误日志
      log.error("Error", e);
      }
    });
在这种情况下,我们的父线程在占有了线程池的所有线程之后,仍然向线程池去提交任务并且使用get方法来获取其运行结果。
此时这个任务就会一直进入内部等待队列,等待父线程让出线程供自己执行。而父线程只有在自身任务执行完毕之后才会释放线程供子线程执行任务。在这种情况下就造成了线程池死锁
我们要如何解决这个问题呢?
其实有一个最简单的解法:既然线程池死锁之后导致有界阻塞队列被占满,引发后续任务一直触发淘汰策略,那我们选择无界阻塞队列不就好了?
后续任务尽管不会被执行,但你就一直往阻塞队列里面加就完事了。
[ Java 中,无界阻塞队列(Unbounded Blocking Queue)是一种特殊的数据它实现了BlockingQueue接口。与有界阻塞队列不同,无界阻塞队列理论上可以存储限数量的元素。向无界阻塞队列中添加元素时,它不会因为队列已满而阻塞(除非遇到系统资源限制,如内存不足等极情况)]
也就图一乐不会真有人在实际开发使用这个吧?那真正的解决方法有哪些呢?
1.使用CompletableFuture的异步回调,避免阻塞线程:
ExecutorService pool = Executors.newSingleThreadExecutor();

        pool.submit(() -> {
            try {
                log.info("First");
                // 使用CompletableFuture异步提交子任务,并在子任务完成后执行后续逻辑
                CompletableFuture.runAsync(() -> log.info("Second"), pool)
                     .thenRun(() -> log.info("Third"));
            } catch (Exception e) {
                log.log(Level.SEVERE, "Error", e);
            }
        });
[CompletableFuture.runAsync是 Java 8 引入的CompletableFuture类中的一个静态方法。它用于以异步的方式执行一个Runnable任务,即这个任务会在一个独立于当前线程的线程中执行。这种异步执行机制可以提高程序的并发性能,避免当前线程因为等待任务完成而被阻塞,使程序能够同时处理多个任务。]
使用了这个方法后,父线程就不会阻塞等待子线程调用的结果,这样就可以在执行完父线程后,让渡线程给子线程来执行任务。
2.拆分线程池,禁止父子线程共享一个线程池:

    public static void main(String[] args) {
        ExecutorService pool = Executors.newSingleThreadExecutor();

        pool.submit(ThreadPoolExampleRewrittenWithMethods::firstTask);

        pool.shutdown();
    }

    public static void firstTask() {
        try {
            log.info("First");
            secondTask();
            log.info("Third");
        } catch (Exception e) {
            log.log(Level.SEVERE, "Error", e);
        }
    }

    public static void secondTask() {
        ExecutorService pool = Executors.newSingleThreadExecutor();
        pool.submit(() -> log.info("Second"));
        pool.shutdown();
    }
}
拆分线程池的方法最简单高效。但是需要注意的是:尽量不要在java代码中创建过多的线程。过多的线程也会拖慢整个项目的响应速度。

  推荐阅读:
  1. Kotlin不可能取代Java
  2. 面试场景题:一次关于线程池使用场景的讨论。
  3. 线程池是什么?线程池与连接池有什么区别?线程池工作原理是什么?
  4. 面试官:线程池中线程异常后,销毁还是复用?

  5. 冲进了小米,二面速通!

脚本之家
脚本之家(jb51.net)每天提供最新IT类资讯、原创内容、编程开发的教程与经验分享,送书福利天天在等你!
 最新文章