CompletableFuture的使用不当

对Completablefuture的使用不当,在异步请求中又嵌入了异步调用,当依赖服务变慢或请求量变大时,future会相互等待产生死锁。

在Java中,使用CompletableFuture时出现死锁的情况可能与不当的线程同步和锁定有关。下面是一个示例程序,演示了如何使用CompletableFuture造成死锁,并输出类似于您提供的死锁日志。

import java.util.concurrent.CompletableFuture;

public class CompletableFutureDeadlock {

    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) throws Exception {
        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + " acquired lock1");
                // Adding a sleep to ensure both threads try to acquire the second lock simultaneously
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + " acquired lock2");
                }
            }
        });

        CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + " acquired lock2");
                // Adding a sleep to ensure both threads try to acquire the first lock simultaneously
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + " acquired lock1");
                }
            }
        });

        // Wait for both futures to complete
        future1.get();
        future2.get();
    }
}

解释

  1. CompletableFuture.runAsync在不同的线程中异步执行任务。
  2. Thread-0持有lock1,尝试获取lock2
  3. Thread-1持有lock2,尝试获取lock1
  4. 由于两个线程都在等待对方持有的锁,这就造成了死锁。

解决方法

要避免死锁,可以重新设计代码,确保锁的获取顺序一致,或者尽量减少对共享资源的锁定。

要还原一个使用CompletableFuture的不当用法,在异步请求中又嵌入异步调用,导致在依赖服务变慢或请求量变大时产生死锁的场景,可以通过创建两个相互依赖的CompletableFuture任务来实现。下面是一个示例代码:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureDeadlockExample {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // Create two CompletableFuture objects that depend on each other
        CompletableFuture<Void> future1 = new CompletableFuture<>();
        CompletableFuture<Void> future2 = new CompletableFuture<>();

        // Run the first async task
        CompletableFuture.runAsync(() -> {
            try {
                // Simulate a slow dependency
                Thread.sleep(100);
                
                System.out.println("Future1 waiting for Future2");
                // Future1 waits for Future2
                future2.get();
                
                System.out.println("Future1 completed");
                future1.complete(null);
            } catch (InterruptedException | ExecutionException e) {
                future1.completeExceptionally(e);
            }
        });

        // Run the second async task
        CompletableFuture.runAsync(() -> {
            try {
                // Simulate a slow dependency
                Thread.sleep(100);
                
                System.out.println("Future2 waiting for Future1");
                // Future2 waits for Future1
                future1.get();
                
                System.out.println("Future2 completed");
                future2.complete(null);
            } catch (InterruptedException | ExecutionException e) {
                future2.completeExceptionally(e);
            }
        });

        // Wait for both futures to complete
        try {
            CompletableFuture.allOf(future1, future2).join();
        } catch (Exception e) {
            System.out.println("Caught exception: " + e);
        }

        System.out.println("Done");
    }
}

解释

  1. 创建两个CompletableFuture对象future1future2
  2. 第一个异步任务尝试等待future2完成,然后完成future1
  3. 第二个异步任务尝试等待future1完成,然后完成future2
  4. 由于future1future2互相等待对方完成,就会出现死锁。

运行结果

当您运行此程序时,您会发现程序被永久卡住,因为两个CompletableFuture互相等待,导致死锁。

解决方案

避免这种情况的方法是重新设计异步任务的依赖关系,确保不会出现互相等待的情况。可以通过以下方式解决:

  1. 避免在异步任务中等待其他异步任务的结果。
  2. 使用thenComposethenCombine等方法来链式处理异步任务,而不是手动等待它们完成。

例如,使用thenCompose来避免死锁:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureSolution {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(100);
                System.out.println("Future1 completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        CompletableFuture<Void> future2 = future1.thenRunAsync(() -> {
            try {
                Thread.sleep(100);
                System.out.println("Future2 completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Wait for both futures to complete
        CompletableFuture.allOf(future1, future2).join();

        System.out.println("Done");
    }
}

在这个解决方案中,future2依赖于future1,并且使用thenRunAsync来避免直接等待,从而避免了死锁。

如何发现这一问题

JStack 是一个分析 Java 应用程序线程状态的有用工具,特别是在诊断死锁问题时非常有用。以下是如何使用 JStack 分析死锁问题的步骤:

步骤 1:获取 Java 进程 ID

首先,需要获取正在运行的 Java 应用程序的进程 ID(PID)。可以使用以下命令找到 PID:

jps -l

这个命令将列出所有运行的 Java 进程及其 PID。例如:

12345 com.example.MyApplication

这里 12345 是 Java 应用程序的 PID。

步骤 2:生成线程转储

使用 jstack 命令生成指定 Java 进程的线程转储:




jstack -l 12345 > thread_dump.txt

这将生成一个包含所有线程状态的文件 thread_dump.txt

步骤 3:分析线程转储

打开生成的 thread_dump.txt 文件,查找死锁信息。通常,如果存在死锁,jstack 输出中会有类似以下的信息:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f9c00000008 (object 0x00007f9c00003010, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x00007f9c00000010 (object 0x00007f9c00003020, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at MyClass.methodA(MyClass.java:10)
        - waiting to lock <0x00007f9c00003010> (a java.lang.Object)
        - locked <0x00007f9c00003020> (a java.lang.Object)
        at MyClass.run(MyClass.java:20)
"Thread-2":
        at MyClass.methodB(MyClass.java:15)
        - waiting to lock <0x00007f9c00003020> (a java.lang.Object)
        - locked <0x00007f9c00003010> (a java.lang.Object)
        at MyClass.run(MyClass.java:25)


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *