楼层: 首页/ 软件技术/ Java 基础/ 并发编程:让程序同时干多件事
08

并发编程:让程序同时干多件事

Threads · ExecutorService · Locks · Virtual Threads

单核时代程序一次只干一件事,像一个厨师炒菜。多核时代你要开多个线程(Thread),像开多个灶台同时炒。但线程不是免费的:开多了抢 CPU、抢数据,就会出现"两个线程同时改一个变量,结果对不上"的竞态条件。这一章讲怎么安全地开多线程。

创建线程的正确姿势:别自己 new Thread

原始做法 → new Thread().start()
每个任务 new 一个线程,线程用完就扔。
问题:线程创建很贵(要向操作系统申请),来一万个请求就开一万个线程,直接把内存撑爆。这是新手最常见的性能事故。
推荐做法 → 线程池 ExecutorService
预先养好一批线程,任务来了丢给它们复用。
好处:复用线程、控制并发数、排队削峰。生产环境一律用线程池,不要手动 new Thread。

线程池 + 两个常见工具

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class ConcurrencyDemo {
    public static void main(String[] args) throws Exception {
        // 固定大小线程池:最多同时跑 4 个任务
        ExecutorService pool = Executors.newFixedThreadPool(4);

        // 原子类:多线程自增不会丢数,比 synchronized 轻
        AtomicInteger count = new AtomicInteger(0);

        // CountDownLatch:等 10 个任务都干完再往下走
        CountDownLatch latch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            pool.submit(() -> {
                try {
                    Thread.sleep(100);      // 模拟干活
                    count.incrementAndGet();
                } catch (InterruptedException e) { throw new RuntimeException(e); }
                finally {
                    latch.countDown();  // 我干完了,计数器减一
                }
            });
        }

        latch.await();             // 等所有任务完成
        pool.shutdown();          // 用完必须关池
        System.out.println("最终 count = " + count.get()); // 10
    }
}

两把锁:synchronized vs ReentrantLock

synchronizedReentrantLock
怎么用关键字,加在方法或代码块上,自动释放。类,手动 lock() / unlock(),必须放 finally。
优点简单,不会忘释放。可公平锁、可尝试 tryLock、可中断,更灵活。
什么时候用大多数普通互斥场景。需要"试一下拿不到锁就先干别的"等高级场景。

volatile 与内存可见性 + 虚拟线程(Java 21+)

// volatile:告诉 JVM 这个变量别缓存,每次都去主内存读
// 典型用途:一个线程写标记,另一个线程读标记来决定停不停
private static volatile boolean running = true;

// Java 21 正式版:虚拟线程(Virtual Threads)——轻量级线程
// 以前一个请求一个操作系统线程,几千个就扛不住;
// 虚拟线程由 JVM 管理,开几十万个也不慌,写法和普通线程几乎一样
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10000; i++) {
        int finalI = i;
        executor.submit(() -> {
            System.out.println("虚拟线程跑任务:" + finalI);
            return null;
        });
    }
}   // try 结束自动关闭并等待所有任务完成

完整线程池案例:ExecutorService + Callable + Future + CompletableFuture 链式调用

import java.util.concurrent.*;

public class ThreadPoolFullDemo {
    public static void main(String[] args) throws Exception {

        // ① Callable + Future:提交一个有返回值的任务
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Future<String> future = pool.submit(() -> {
            Thread.sleep(500);
            return "任务结果";
        });
        System.out.println("干别的...");
        String result = future.get();      // 阻塞等结果
        System.out.println(result);
        // 干别的...
        // 任务结果

        // ② CompletableFuture 链式:thenApply / thenCombine
        CompletableFuture<Integer> cf = CompletableFuture
            .supplyAsync(() -> 1, pool)           // 异步拿到 1
            .thenApply(x -> x + 10)              // 加 10 → 11
            .thenApply(x -> x * 2)               // 乘 2 → 22
            .thenCombine(                             // 和另一个任务合并
                CompletableFuture.supplyAsync(() -> 100, pool),
                (a, b) -> a + b                     // 22 + 100 = 122
            );
        System.out.println(cf.get());
        // 122

        // ③ allOf:等多个任务全部完成
        CompletableFuture<Void> all = CompletableFuture.allOf(
            CompletableFuture.runAsync(() -> sleep(200), pool),
            CompletableFuture.runAsync(() -> sleep(300), pool),
            CompletableFuture.runAsync(() -> sleep(100), pool)
        );
        all.join();                               // 等三个都结束
        System.out.println("三个任务都干完了");
        // 三个任务都干完了

        pool.shutdown();
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

虚拟线程实战(JDK 21+):一万个请求一万个线程,轻松跑

// 传统平台线程:开 10000 个线程直接 OOM / unable to create new native thread
// 虚拟线程:JVM 调度,开几十万个也不慌,写法和普通线程一样
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10000; i++) {
        int no = i;
        executor.submit(() -> {
            // 每个虚拟线程模拟一个 HTTP 请求:睡 100ms
            Thread.sleep(100);
            return null;
        });
    }
} // try 关闭时自动等所有虚拟线程跑完
// 总耗时约 100ms(并发跑),不是 10000 × 100ms

线程的一生:六种状态

jstack 排查线程问题时满屏 BLOCKED、WAITING,先知道每个状态在干嘛,才看得懂。

状态人话
NEW刚 new 出来,还没 start。
RUNNABLE正在跑,或等着 CPU 时间片。
BLOCKED在等别人释放 synchronized 锁。
WAITING无限期等人唤醒(wait / join)。
TIMED_WAITING等一会儿(sleep(1000))。
TERMINATED跑完了,死了。

ThreadLocal:每个线程一份自己的副本

Spring 把当前用户、事务绑定到"当前线程"上,靠的就是 ThreadLocal。大白话:一个 Map,key 是当前线程,value 是你存的东西。A 线程 set 的,B 线程根本看不到,天然线程安全。

// 每个线程存一份自己的"当前用户",互不干扰
static final ThreadLocal<String> CURRENT_USER = new ThreadLocal<>();

public static void main(String[] args) throws Exception {
    CURRENT_USER.set("主线程用户");
    Thread t = new Thread(() -> {
        CURRENT_USER.set("子线程用户");
        System.out.println(CURRENT_USER.get());   // 子线程用户
        CURRENT_USER.remove();                  // 用完一定 remove!线程池里不删会泄漏
    });
    t.start();
    t.join();
    System.out.println(CURRENT_USER.get());       // 主线程用户
}
ThreadLocal 在线程池里必踩的坑

线程池的线程是复用的,你 set 完不 remove,下一个任务拿到这个线程时,会读到上一个任务残留的值——比如把订单归属到上一个用户头上。铁律:ThreadLocal 用完一定 remove(),最好放 finally。

并发集合:别在多线程里用 HashMap

普通 HashMap、ArrayList 遇到多线程同时写,轻则数据错乱,重则 JDK 7 直接死循环(JDK 8 修了但仍不安全)。java.util.concurrent 包给了线程安全版。

并发集合替代谁 / 什么时候用
ConcurrentHashMap替代 HashMap。分段锁(JDK8 后用 CAS+synchronized 桶级锁),读几乎无锁,日常并发 Map 首选。
CopyOnWriteArrayList替代 ArrayList。写时复制一份新数组,读不加锁。适合"读多写极少"(如监听器列表),写频繁别用。
BlockingQueue线程池的队列、生产者-消费者模型专用,put/take 自动阻塞等待。
Map<String, Integer> safeMap = new ConcurrentHashMap<>();
// 多线程同时 put 不会死循环、不会丢数据
IntStream.range(0, 1000).parallel().forEach(i ->
    safeMap.put("k" + i, i)
);

结构化并发:虚拟线程的正确打开方式(JDK 21+)

开一堆虚拟线程去查数据,最大的痛点是"不知道它们什么时候全完事、某个炸了怎么办"。结构化并发(Structured Concurrency,预览特性)用 StructuredTaskScope 把一组子线程当成一个整体:要么全部成功,要么一个失败其他全部取消,作用域结束自动等齐。

// 并行查用户和订单,两个都回来才往下走;一个失败自动取消另一个
try (var scope = StructuredTaskScope.open()) {
    Subtask<User> user  = scope.fork(() -> fetchUser(id));
    Subtask<Order> order = scope.fork(() -> fetchOrder(id));
    scope.join();        // 等两个都完成;默认策略下任一失败自动抛异常
    System.out.println(user.get() + order.get());
} // try 结束:所有虚拟线程保证都已结束,不会泄漏

论它解决了什么老问题

以前手动 fork 一堆线程再 join,得自己 try-catch、自己设超时、自己记得 join 每个,漏一个线程就"飞了"。结构化并发把"开任务"和"等任务、取消任务"绑在同一个语法块里,作用域一结束,编译期就保证子线程都被收编。版本说明:结构化并发在 JDK 21/24 为预览特性,API 可能随版本微调,以官网最新稳定版为准。

竞态条件经典翻车:count++ 不是原子操作

两个线程同时执行 count++,结果可能少加一次。因为 count++ 底层是"读-改-写"三步,两个线程可能同时读到同一个旧值。解决办法:用 AtomicInteger(原子类,无锁),或用 synchronized / ReentrantLock 加锁。写并发代码的第一原则:共享可变变量,要么加锁,要么用并发包工具类。

章末面试 · 并发编程(5 题)

1.(概念题)synchronized 和 ReentrantLock 区别?什么时候用后者?

查看答案

答案:synchronized 是关键字自动释放,简单不会忘;ReentrantLock 是类手动 lock/unlock(必须放 finally),支持公平锁、tryLock 超时、可中断。需要"拿不到锁就先干别的"或公平排队时用 ReentrantLock。

2.(概念题)volatile 解决什么问题?它是原子操作吗?

查看答案

答案:volatile 保证可见性(一个线程写了,其他线程立刻看到)和禁止指令重排,但不保证原子性。所以 volatile 不能替代 synchronized 做 count++。典型用途:状态标记(running = false 让另一个线程停)。

3.(选择题)线程池核心参数里,哪个决定"最多同时跑几个任务"?A. keepAliveTime B. corePoolSize / maximumPoolSize C. queueCapacity

查看答案

答案:B。corePoolSize 常驻线程数,maximumPoolSize 最大线程数。队列满了才会扩到 max。

4.(概念题)为什么生产环境不要用 Executors.newCachedThreadPool()?

查看答案

答案:它的最大线程数是 Integer.MAX_VALUE,来多少请求开多少线程,直接 OOM。阿里手册明令禁止。手动用 ThreadPoolExecutor 显式指定参数。

5.(新特性题)虚拟线程和传统线程有什么区别?

查看答案

答案:传统平台线程是对操作系统线程的薄封装,一个线程占 1MB 栈,开几千就扛不住;虚拟线程由 JVM 调度,跑在少量载体线程上,一个虚拟线程只占几百字节栈,开几十万个不慌。写法一样,但不要再用线程池缓存虚拟线程。