Java多线程任务编排:结构化并发 vs CountDownLatch

243 阅读3分钟

在处理复杂任务依赖关系时,Java 提供了多种多线程实现方式。本文将以一个典型任务依赖图为例,对比分析结构化并发(Structured Concurrency)与传统 CountDownLatch 两种实现方式的优劣。

任务依赖图分析

以下面的任务依赖图为例:

A   B
 \ /
  C
 / \
D   E
|   / \
|  F   G
 \ | /
   H

任务依赖关系如下:

  • A 和 B 没有依赖,可以并行执行
  • C 依赖 A 和 B
  • D 和 E 依赖 C
  • F 和 G 依赖 E
  • H 依赖 D、F 和 G

传统 CountDownLatch 实现

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(8);
        
        // 创建所有任务的 latch
        CountDownLatch latchA = new CountDownLatch(1);
        CountDownLatch latchB = new CountDownLatch(1);
        CountDownLatch latchC = new CountDownLatch(1);
        CountDownLatch latchD = new CountDownLatch(1);
        CountDownLatch latchE = new CountDownLatch(1);
        CountDownLatch latchF = new CountDownLatch(1);
        CountDownLatch latchG = new CountDownLatch(1);
        CountDownLatch latchH = new CountDownLatch(1);

        // 任务A - 无依赖
        executor.submit(() -> {
            try {
                runTask("A", 100);
                latchA.countDown();
            } catch (InterruptedException ex) {
            }
        });
        
        // 任务B - 无依赖
        executor.submit(() -> {
            try {
                runTask("B", 150);
                latchB.countDown();
            } catch (InterruptedException ex) {
            }
        });
        
        // 任务C - 依赖A和B
        executor.submit(() -> {
            try {
                latchA.await();
                latchB.await();
                runTask("C", 200);
                latchC.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // 任务D - 依赖C
        executor.submit(() -> {
            try {
                latchC.await();
                runTask("D", 500);
                latchD.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // 任务E - 依赖C
        executor.submit(() -> {
            try {
                latchC.await();
                runTask("E", 120);
                latchE.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // 任务F - 依赖E
        executor.submit(() -> {
            try {
                latchE.await();
                runTask("F", 80);
                latchF.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // 任务G - 依赖E
        executor.submit(() -> {
            try {
                latchE.await();
                runTask("G", 90);
                latchG.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 任务H - 依赖D, F, G
        executor.submit(() -> {
            try {
                latchD.await();
                latchF.await();
                latchG.await();
                runTask("H", 50);
                latchH.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // 等待所有任务完成
        latchH.await();
        executor.shutdown();

        System.out.println("All tasks completed successfully");
    }
    
    private static Void runTask(String name, long duration) throws InterruptedException {
        System.out.println("Task " + name + " running");
        Thread.sleep(duration);
        System.out.println("Task " + name + " completed");
        return null;
    }
}

传统实现特点:

  • 需要显式创建和管理多个 CountDownLatch
  • 每个任务的依赖关系通过 await()方法显式声明
  • 线程池需要手动管理生命周期
  • 错误处理需要单独实现

结构化并发

结构化并发(Structured Concurrency)是 Java 19 引入的一种并发编程范式,旨在通过代码结构显式表达任务的生命周期和依赖关系,从而简化多线程开发。其核心思想是:子任务的执行必须在其父任务的上下文中完成,避免"线程泄漏"和复杂的同步控制。

import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;

public class StructuredConcurrency {
    public static void main(String[] args) {
        try {
            // 阶段1: A和B并行
            try (var scopeAB = new StructuredTaskScope.ShutdownOnFailure()) {
                Subtask<Void> a = scopeAB.fork(() -> runTask("A", 100));
                Subtask<Void> b = scopeAB.fork(() -> runTask("B", 150));
                
                scopeAB.join().throwIfFailed();
                
                // 阶段2: C执行
                runTask("C", 200);
                    
                // 阶段3: D和E并行
                try (var scopeDE = new StructuredTaskScope.ShutdownOnFailure()) {
                    Subtask<Void> d = scopeDE.fork(() -> runTask("D", 500));
                    
                    // E完成后立即触发F和G,不需要等待D
                    Subtask<Void> e = scopeDE.fork(() -> {
                        runTask("E", 120);
                        
                        // E完成后立即启动F和G
                        try (var scopeFG = new StructuredTaskScope.ShutdownOnFailure()) {
                            Subtask<Void> f = scopeFG.fork(() -> runTask("F", 80));
                            Subtask<Void> g = scopeFG.fork(() -> runTask("G", 90));
                            
                            scopeFG.join().throwIfFailed();
                        }
                        return null;
                    });
                    
                    scopeDE.join().throwIfFailed();

                    // 阶段4: H执行(需要D, F, G完成)
                    // 注意: F和G已经在E的任务中完成
                    runTask("H", 50);
                }
            }
            
            System.out.println("All tasks completed successfully");
        } catch (Exception e) {
            System.err.println("Task failed: " + e.getMessage());
        }
    }
    
    private static Void runTask(String name, long duration) throws InterruptedException {
        System.out.println("Task " + name + " running");
        Thread.sleep(duration);
        System.out.println("Task " + name + " completed");
        return null;
    }
}

结论

对比两种实现,可以发现结构化并发的代码短很多。此外,结构化并发还有以下优势:

  1. 取消传播:父任务取消会自动传播到所有子任务
  2. 短路错误处理:如果某个子任务失败,其它子任务会自动被取消
  3. 逻辑直观:任务依赖关系通过代码结构自然表达,无需显式同步机制