46. 【Java】JUC并发工具:让并发更简单

摘要: 本文深入解析 Java 并发编程的核心工具集 JUC(java.util.concurrent),涵盖线程池、阻塞队列、同步辅助类等关键组件。通过实际代码示例,详细讲解如何利用 ExecutorService 管理线程生命周期、使用 BlockingQueue 实现线程安全队列、借助 CountDownLatch、CyclicBarrier 和 Semaphore 协调线程执行,以及 Fork/Join 框架和 CompletableFuture 的高级应用。掌握这些工具能显著提升并发编程效率,让代码从"手工打造"升级为"标准化生产"。

关键词: Java并发编程, JUC工具集, 线程池, 阻塞队列, CompletableFuture


在过去的几篇文章里,我们从线程的创建开始,学到了 synchronizedLock 来保护共享数据,又学了 wait()/notify()Condition 来实现线程间的协作。

你现在应该对"多线程怎么工作"有了比较深刻的理解。

但是,你也可能感觉到了:这些工具用起来有点"原始"。

  • 每次需要执行一个异步任务,都要手动 new Thread(...).start(),创建和销毁线程的开销不小。
  • synchronized 保护共享数据,用 wait()/notify() 协调线程,代码写起来繁琐,稍不注意就会引入死锁或性能问题。
  • 如果我们需要限制同时访问某个资源的线程数量(比如数据库连接池最多10个连接),用 synchronized 来实现会非常复杂。

好在,Java 的设计者早就想到了这些。从 Java 5 开始,java.util.concurrent(简称 JUC)包被引入,它提供了一整套现成的、经过充分测试的高性能并发工具

今天,我们就来认识这些工具中最常用、最核心的几个:

  • 线程池(ExecutorService ------ 管理线程的生命周期,复用线程。
  • 阻塞队列(BlockingQueue ------ 线程安全的队列,自带等待通知。
  • 同步辅助类(CountDownLatchCyclicBarrierSemaphore ------ 让线程之间可以"协同"完成某个任务。
  • Fork/Join 框架 ------ 专门用于分治任务的并行计算框架。

这些工具大大简化了并发编程的复杂度。学会它们,你的并发代码会从"手工打造"变成"使用标准化零件"的现代化生产。


1. 线程池 ------ 管理线程的"管家"

在之前的例子中,我们每次需要执行任务时,都会 new Thread(task).start()。这在任务数量少的时候没问题,但如果你的应用需要创建成千上万个线程(比如一个 Web 服务器要处理数万个并发请求),频繁创建和销毁线程会带来巨大的开销------每次创建线程都要分配栈内存、注册到操作系统,销毁时也要释放资源。

线程池 的核心思想是:预先创建好一批线程,让它们"随时待命"。当有任务需要执行时,从池子里取出一个空闲线程来执行任务;任务执行完后,线程不销毁,而是回到池子里等待下一个任务。这样就复用了线程,大大减少了创建和销毁的开销。

1.1 创建线程池 ------ Executors 工厂类

java.util.concurrent.Executors 提供了几种常用的线程池工厂方法:

java 复制代码
import java.util.concurrent.*;

public class ExecutorsDemo {
    public static void main(String[] args) {
        // 1. 固定大小的线程池(核心线程数 = 最大线程数)
        ExecutorService fixedPool = Executors.newFixedThreadPool(5);
        
        // 2. 单线程的线程池(只有一个线程,保证任务按顺序执行)
        ExecutorService singlePool = Executors.newSingleThreadExecutor();
        
        // 3. 可缓存的线程池(核心线程数为0,最大线程数无限,空闲线程60秒后回收)
        ExecutorService cachedPool = Executors.newCachedThreadPool();
        
        // 4. 定时任务线程池(支持延时执行和周期性执行)
        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
        
        // 5. 工作窃取线程池(Java 8+,基于 Fork/Join 框架)
        ExecutorService workStealingPool = Executors.newWorkStealingPool();
    }
}

建议 :在阿里巴巴 Java 开发手册中,不推荐直接使用 Executors 工厂方法创建线程池,因为某些方法(如 newFixedThreadPoolnewCachedThreadPool)使用的是无界队列或无限创建线程,可能在某些极端情况下导致 OOM。更安全的方式是直接用 ThreadPoolExecutor 的构造方法,显式指定所有参数。我们在本篇文章中为了演示简洁,先用 Executors,但在生产环境中建议使用 ThreadPoolExecutor 显式构造。

1.2 提交任务 ------ execute()submit()

有了线程池,提交任务非常简单:

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(3);

// 提交无返回值的任务(Runnable)
pool.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " 执行任务");
});

// 提交有返回值的任务(Callable)
Future<Integer> future = pool.submit(() -> {
    Thread.sleep(1000);
    return 42;
});

try {
    Integer result = future.get();  // 阻塞等待结果
    System.out.println("结果:" + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

// 关闭线程池(不再接受新任务,等待已有任务完成)
pool.shutdown();  // 优雅关闭
// pool.shutdownNow();  // 立即尝试关闭

execute() vs submit()

  • execute(Runnable):提交任务,不关心返回值,无返回结果。
  • submit(Callable)submit(Runnable):提交任务,返回 Future<T>,可以获取执行结果或取消任务。
1.3 Future ------ 异步任务的"结果凭证"

Future 代表一个异步计算的结果 。你可以在提交任务后继续做其他事情,然后在需要结果的时候调用 get() 来等待结果。

java 复制代码
Future<String> future = pool.submit(() -> {
    // 模拟耗时操作
    Thread.sleep(2000);
    return "完成!";
});

// 做其他事情...
System.out.println("主线程继续执行...");

// 稍后获取结果(会阻塞)
String result = future.get(3, TimeUnit.SECONDS);  // 最多等待3秒,超时抛出 TimeoutException
System.out.println(result);

Future 的常用方法:

方法 说明
get() 阻塞等待结果,直到任务完成
get(long timeout, TimeUnit unit) 带超时的等待
isDone() 检查任务是否已完成
isCancelled() 检查任务是否已被取消
cancel(boolean mayInterruptIfRunning) 尝试取消任务

注意:Future.get() 会阻塞当前线程。如果需要异步回调,可以配合 CompletableFuture(我们会在后面简要提到)。

1.4 ThreadPoolExecutor ------ 更精细的配置

如果你需要更精细地控制线程池的行为,可以用 ThreadPoolExecutor 的构造方法:

java 复制代码
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2,                              // 核心线程数(始终保留)
    5,                              // 最大线程数
    60,                             // 空闲线程存活时间
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(10),  // 任务队列(容量10)
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用者线程执行
);

// 参数解读:
// - 当任务数 ≤ 2 时,只使用核心线程。
// - 当任务数 > 2 但 ≤ 12(2个核心 + 10个队列)时,新任务进入队列等待。
// - 当任务数 > 12 时,创建新线程(最多到5个)来执行任务。
// - 当任务数 > 15(5个最大线程 + 10个队列)时,触发拒绝策略。

拒绝策略(RejectedExecutionHandler)

  • AbortPolicy(默认):抛出 RejectedExecutionException
  • CallerRunsPolicy:由提交任务的线程自己执行该任务(有"反压"作用)。
  • DiscardPolicy:丢弃任务,不抛异常。
  • DiscardOldestPolicy:丢弃队列中最旧的任务,再尝试提交新任务。

2. 阻塞队列 ------ BlockingQueue

我们在上一篇文章中已经简单提到了 BlockingQueue。它是 JUC 包中提供的一种线程安全的队列 ,在队列为空时,take() 操作会阻塞;在队列已满时,put() 操作会阻塞。

BlockingQueue 的主要实现:

实现类 特点
ArrayBlockingQueue 有界队列,基于数组,FIFO
LinkedBlockingQueue 可选有界(默认无界),基于链表,FIFO
PriorityBlockingQueue 无界优先队列,元素按自然顺序或 Comparator 排序
SynchronousQueue 容量为0,每个 put() 必须等待一个 take()
DelayQueue 无界队列,元素只有延迟期满才能被取出
LinkedTransferQueue 无界,支持 transfer()(直接交给消费者)
java 复制代码
// 生产者-消费者模式的一个典型用法
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

// 生产者
Thread producer = new Thread(() -> {
    try {
        for (int i = 0; i < 10; i++) {
            queue.put("消息-" + i);  // 队列满时阻塞
            System.out.println("生产:" + i);
            Thread.sleep(200);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

// 消费者
Thread consumer = new Thread(() -> {
    try {
        for (int i = 0; i < 10; i++) {
            String msg = queue.take();  // 队列空时阻塞
            System.out.println("消费:" + msg);
            Thread.sleep(300);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

producer.start();
consumer.start();

BlockingQueue 的方法分为四类(取决于是否阻塞、是否抛出异常):

操作 抛异常 返回特殊值 阻塞 超时阻塞
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek() --- ---

在实际开发中,put()/take() 是最常用的组合。


3. 同步辅助类 ------ 让线程"协同"

JUC 提供了几个同步辅助类,它们本身不存储数据,而是用来协调多个线程的执行节奏。

3.1 CountDownLatch ------ 等待"门闩"打开

CountDownLatch 允许一个或多个线程等待其他线程完成各自的操作 。它有一个计数器,初始值为 N。每当一个线程完成了自己的任务,调用 countDown(),计数器减 1。当计数器减到 0 时,所有在 await() 上等待的线程被唤醒。

典型场景:一个服务启动时,需要等待多个前置组件初始化完成后再对外提供服务。

java 复制代码
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 3;
        CountDownLatch latch = new CountDownLatch(taskCount);

        // 启动 3 个线程执行任务
        for (int i = 1; i <= taskCount; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    System.out.println("任务 " + id + " 开始执行...");
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println("任务 " + id + " 执行完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();  // 计数器减1
                }
            }).start();
        }

        System.out.println("主线程等待所有任务完成...");
        latch.await();  // 阻塞,直到计数器变为0
        System.out.println("所有任务已完成,主线程继续执行!");
    }
}

CountDownLatch 是一次性的------计数器减到 0 后就不能再重置了。如果需要重复使用,请看下面的 CyclicBarrier

3.2 CyclicBarrier ------ 循环屏障

CyclicBarrier 让一组线程互相等待 ,直到所有线程都到达某个"屏障点"后,才一起继续执行。它和 CountDownLatch 的区别是:

  • CountDownLatch 是"等待其他线程完成",是一个线程等一组线程。
  • CyclicBarrier 是"所有线程互相等待",是一组线程互相等。
  • CyclicBarrier 可以重置reset()),可以重复使用。

典型场景:多个线程分段处理一个大数据集,每处理完一段后等待其他线程处理完,然后汇总。

java 复制代码
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        int threadCount = 3;
        // 所有线程到达屏障后,执行一个汇总动作(可选)
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程已到达屏障,执行汇总!");
        });

        for (int i = 1; i <= threadCount; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    System.out.println("线程 " + id + " 开始工作...");
                    Thread.sleep((long) (Math.random() * 3000));
                    System.out.println("线程 " + id + " 到达屏障,等待其他线程...");
                    barrier.await();  // 等待其他线程
                    System.out.println("线程 " + id + " 越过屏障,继续执行!");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

如果某个线程在 await() 时被中断或被超时,屏障会被"破坏"(Broken),其他线程会抛出 BrokenBarrierException。这是一种"全员退出"的机制,避免一个线程出问题时其他线程永远等待。

3.3 Semaphore ------ 限流"信号量"

Semaphore(信号量)用来控制同时访问某个资源的线程数量 。它有一个许可(permit)计数,线程通过 acquire() 获取许可,如果许可不足则阻塞;通过 release() 释放许可。

典型场景:数据库连接池(最多 10 个连接)、限流器(限制 QPS)。

java 复制代码
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 限制同时最多 3 个线程访问
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 10; i++) {
            final int id = i;
            new Thread(() -> {
                try {
                    // 尝试获取许可(如果获取不到,阻塞)
                    semaphore.acquire();
                    System.out.println("线程 " + id + " 获得许可,开始工作...");
                    Thread.sleep((long) (Math.random() * 2000));
                    System.out.println("线程 " + id + " 工作完成,释放许可");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }).start();
        }
    }
}

Semaphore 也支持 tryAcquire()(尝试获取,不阻塞)和带超时的 tryAcquire(timeout, unit),非常灵活。


4. Fork/Join 框架 ------ 分治并行

Fork/Join 框架是 Java 7 引入的,专门用于可以递归分解的任务(分治算法)。它的核心思想是:

  1. Fork(分):把一个大任务拆分成若干个小任务。
  2. Join(合):把小任务的结果合并成大任务的结果。

典型的应用场景是并行计算,比如求一个巨大数组的和、快速排序的并行版本等。

java 复制代码
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinDemo {
    // 求 1 到 N 的和(用分治方式)
    static class SumTask extends RecursiveTask<Long> {
        private final long from;
        private final long to;
        private static final long THRESHOLD = 100_000;  // 阈值:如果范围小于这个值,直接计算

        SumTask(long from, long to) {
            this.from = from;
            this.to = to;
        }

        @Override
        protected Long compute() {
            if (to - from <= THRESHOLD) {
                // 直接计算
                long sum = 0;
                for (long i = from; i <= to; i++) {
                    sum += i;
                }
                System.out.println(Thread.currentThread().getName() + " 计算 " + from + " ~ " + to + " = " + sum);
                return sum;
            } else {
                // 分解任务
                long mid = (from + to) / 2;
                SumTask left = new SumTask(from, mid);
                SumTask right = new SumTask(mid + 1, to);
                // 使用 fork() 异步执行子任务
                left.fork();
                // 同步执行右任务
                long rightResult = right.compute();
                // 等待左任务完成并获取结果
                long leftResult = left.join();
                return leftResult + rightResult;
            }
        }
    }

    public static void main(String[] args) {
        // 创建一个 ForkJoinPool
        ForkJoinPool pool = new ForkJoinPool();
        long n = 1_000_000L;
        long result = pool.invoke(new SumTask(1, n));
        System.out.println("1 到 " + n + " 的和 = " + result);
        pool.shutdown();
    }
}

ForkJoinPool 使用**工作窃取(Work-Stealing)**算法:每个线程都有自己的任务队列,如果某个线程完成了自己的任务,它会从其他线程的队列尾部"偷取"任务来执行,从而提高 CPU 利用率。

JDK 8 中 Arrays.parallelSort()Stream.parallel() 等内部都使用了 Fork/Join 框架。


5. CompletableFuture ------ 异步编程的"瑞士军刀"

CompletableFuture 是 Java 8 引入的,它是对 Future 的增强,支持回调链式组合。它让异步编程变得极其灵活。

java 复制代码
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        // 1. 创建异步任务
        CompletableFuture.supplyAsync(() -> {
            // 模拟耗时操作
            sleep(1000);
            return "Hello";
        }).thenApply(result -> {
            // 转换结果
            return result + " World";
        }).thenAccept(result -> {
            // 消费结果
            System.out.println("结果:" + result);
        }).exceptionally(ex -> {
            // 处理异常
            System.err.println("出错了:" + ex.getMessage());
            return null;
        });

        // 2. 组合多个 CompletableFuture
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            sleep(500);
            return 10;
        });
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            sleep(800);
            return 20;
        });

        // 等待两个任务都完成,然后合并结果
        CompletableFuture<Integer> combined = future1.thenCombine(future2, (a, b) -> a + b);
        System.out.println("合并结果:" + combined.get());  // 30

        // 等待任意一个完成(谁先完成用谁)
        CompletableFuture<Object> any = CompletableFuture.anyOf(future1, future2);
        System.out.println("先完成的:" + any.get());

        // 注意:主线程不要过早退出,否则异步任务可能没执行完
        Thread.sleep(2000);
    }

    private static void sleep(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

CompletableFuture 支持的方法非常丰富,包括 thenApply(转换)、thenAccept(消费)、thenRun(执行Runnable)、thenCombine(合并)、anyOf(任意一个完成)、allOf(全部完成)等。它让 Java 的异步编程体验接近了 JavaScript 的 Promise。


6. 综合示例 ------ 并行下载器

让我们用今天学到的工具,实现一个简单的"并行下载器"模拟程序:同时下载多个文件,等待所有下载完成后再统一处理。

java 复制代码
import java.util.concurrent.*;
import java.util.*;

public class ParallelDownloader {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 模拟文件列表
        List<String> files = Arrays.asList(
            "file1.zip", "file2.zip", "file3.zip", "file4.zip", "file5.zip"
        );

        // 使用线程池(每个文件一个线程并行下载)
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 用 CompletableFuture 处理异步下载
        List<CompletableFuture<String>> futures = files.stream()
            .map(file -> CompletableFuture.supplyAsync(() -> downloadFile(file), executor))
            .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

        // 等待所有下载完成,用 allOf
        CompletableFuture<Void> allDone = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0])
        );

        // 所有下载完成后,汇总结果
        CompletableFuture<List<String>> resultFuture = allDone.thenApply(v ->
            futures.stream()
                .map(CompletableFuture::join)  // 这里 join 不会阻塞,因为都已经完成了
                .collect(Collectors.toList())
        );

        // 打印结果
        System.out.println("所有下载任务已提交,等待完成...");
        List<String> results = resultFuture.get();
        System.out.println("下载完成!");
        results.forEach(System.out::println);

        executor.shutdown();
    }

    private static String downloadFile(String filename) {
        try {
            // 模拟下载耗时(随机 1~3 秒)
            long time = (long) (Math.random() * 2000 + 1000);
            Thread.sleep(time);
            System.out.println(Thread.currentThread().getName() + " 下载 " + filename + " 完成,耗时 " + time + "ms");
            return filename + " (大小: " + (time / 100) + "KB)";
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return filename + " 下载失败";
        }
    }
}

这个例子综合运用了:线程池、CompletableFutureallOfjoin,代码简洁且功能完整。


7. 今天的总结

今天我们学习了 JUC 包中的几个核心并发工具:

工具 用途
ExecutorService(线程池) 管理线程生命周期,复用线程。核心:Executors 工厂方法、submit()Future
BlockingQueue(阻塞队列) 线程安全的队列,自带等待通知。核心:put()take()
CountDownLatch(门闩) 一个/多个线程等待其他线程完成后再执行
CyclicBarrier(屏障) 一组线程互相等待,全部到达屏障点后一起继续
Semaphore(信号量) 控制同时访问某个资源的线程数量
ForkJoinPool(分治框架) 递归分解任务,并行计算后合并结果
CompletableFuture(异步增强) 支持回调、链式组合的 Future

这些工具让并发编程从"手工织布"变成了"使用现代化纺织机"。你不需要再自己管理线程池的细节,不需要再手动写 wait()/notify() 来实现生产者-消费者,不需要再费心思去设计线程协调的逻辑。JUC 已经帮你做好了。


动手试试

  1. ThreadPoolExecutor 显式构造一个线程池(核心线程 2,最大线程 4,队列容量 10),提交 20 个任务,观察哪些任务被拒绝,哪些被丢弃(根据拒绝策略不同)。

  2. BlockingQueue 实现一个简单的"日志队列":多个线程往队列里写日志,一个线程从队列里消费日志并打印(模拟异步日志)。

  3. CountDownLatch 实现一个"并发测试工具":同时启动 10 个线程,让它们在同一个"起跑线"上开始执行(提示:用 CountDownLatch(1) 作为"起跑信号",所有线程 await(),主线程 countDown() 后所有线程同时启动)。

  4. CompletableFuture 实现一个"旅游路线推荐器":并行查询三个平台的酒店价格(模拟耗时 1~3 秒),取最低价返回。

  5. (挑战)用 ForkJoinPool 实现一个并行快速排序(ParallelQuickSort),对一个大型整数数组进行排序,并和 Arrays.sort() 比较性能。

JUC 工具是 Java 并发编程的"标准库"。学会它们,你就能用更少的代码写出更高效、更安全的并发程序。

我们下一篇会深入 JVM 内存模型和可见性问题,理解 volatile 关键字和 happens-before 原则,这是并发编程的"内功心法"。

我们下一篇见。😃

📌 获取本系列示例代码请访问 GitCode

相关推荐
Nebula_g2 小时前
JavaSE基础语法:面向对象高级(代码中的成分)
java·开发语言·编程·javase·技术栈·高级语法
for_ever_love__2 小时前
python基础语法学习: 异常的传递性
开发语言·python·学习
AC赳赳老秦2 小时前
公开图片 OCR 数据提取:OpenClaw 合规采集公开信息图,识别文字与表格并转化为结构化数据
java·大数据·前端·数据库·python·php·openclaw
依然鸣3 小时前
PTA团体程序设计天梯赛L1真题讲解L1-077-080
开发语言·c++·算法·深度优先·pat考试·图论
小强库计算机毕业设计4 小时前
SpringBoot+Vue3 学生宿舍管理系统实战
java·spring boot·后端·vue·学生宿舍管理系统
李冰然4 小时前
深入Java集合框架:HashMap源码解析(JDK 8)
java
库玛西4 小时前
现代 C++ 智能指针全景指南:从 RAII 思想到工业级实践
c语言·开发语言·c++·笔记·面试
liulilittle5 小时前
无锁并发容器的设计与实现原理
开发语言·c++·set·map·并发·无锁·lock-free
人道领域5 小时前
【0-1的agent进阶篇】RAG:从Embedding到检索增强生成的底层逻辑
java·embedding·agent·rag