21、并发与异步重构:线程安全重构、锁粒度优化、异步编程模式(CompletableFuture/async/await)、响应式编程重构

并发与异步,说白了就是让程序「同时干多件事」的能力。但这件事,做起来远比听起来复杂。我见过太多系统,一上高并发就崩,一查全是线程安全问题。今天我们就来聊聊,怎么把那些「跑得慢还容易出bug」的并发代码,重构得既快又稳。

21.1 线程安全重构:从「裸奔」到「穿盔甲」

先问个问题:你的共享变量,真的安全吗?

我遇到过最典型的场景——一个计数器,多个线程同时加1。你以为结果是1000,结果跑出来是998。为什么?因为 count++ 不是原子操作。它分三步:读、加、写。两个线程同时读到同一个值,各自加1再写回去,结果只加了1次。

核心原则:只要有一个线程在写,其他线程在读或写,就必须同步。

21.1.1 不可变对象:最安全的锁

我个人的习惯是:能不用锁,就不用锁。怎么做到?用不可变对象。

// 重构前:可变对象,需要加锁
public class User {
    private String name;
    public void setName(String name) { this.name = name; }
    public String getName() { return name; }
}

// 重构后:不可变对象,天然线程安全
public final class ImmutableUser {
    private final String name;
    public ImmutableUser(String name) { this.name = name; }
    public String getName() { return name; }
}

你想想看,对象一旦创建就不能改,那还有啥线程安全问题?

21.1.2 原子类:CAS 的妙用

如果必须改状态,用 AtomicInteger 代替 synchronized。CAS(Compare-And-Swap)是硬件级别的原子操作,比锁轻量得多。

// 重构前:synchronized 加锁
private int count;
public synchronized void increment() { count++; }

// 重构后:原子类,无锁并发
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }

小提示:CAS 在低竞争场景下性能极好,但高竞争时会导致大量自旋,反而浪费 CPU。这时候可以考虑 LongAdder。

21.2 锁粒度优化:别把整栋楼都锁上

我曾经接手过一个系统,性能瓶颈竟然是一把大锁。一个方法里,只有一行代码需要同步,结果整个方法都加了 synchronized。这就好比你要锁一个抽屉,结果把整栋楼都锁了。

21.2.1 锁粗化 vs 锁细化

策略 适用场景 示例
锁细化 只有小部分代码需要同步 把大 synchronized 方法拆成多个小同步块
锁粗化 频繁加锁解锁,且锁竞争不激烈 把循环内的加锁提到循环外
// 锁细化:只锁需要保护的部分
// 重构前:整个方法加锁
public synchronized void process(User user) {
    // 1. 校验(不需要锁)
    validate(user);
    // 2. 更新(需要锁)
    update(user);
    // 3. 日志(不需要锁)
    log(user);
}

// 重构后:只锁 update 部分
public void process(User user) {
    validate(user);
    synchronized(this) {
        update(user);
    }
    log(user);
}

21.2.2 读写锁:读多写少的利器

我建议,如果你的场景是「读远多于写」,用 ReadWriteLock。多个线程可以同时读,但写的时候必须独占。

private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private Map<String, Object> cache = new HashMap<>();

public Object get(String key) {
    readLock.lock();
    try {
        return cache.get(key);
    } finally {
        readLock.unlock();
    }
}

public void put(String key, Object value) {
    writeLock.lock();
    try {
        cache.put(key, value);
    } finally {
        writeLock.unlock();
    }
}

注意:读写锁不是银弹。如果写操作频繁,读线程会被频繁阻塞,性能反而不如普通互斥锁。

21.3 异步编程模式:从「等」到「回调」

传统的同步编程,就像你去餐厅点餐,然后站在柜台前干等。异步编程呢?你点完餐,拿个号,找个座位坐下,该干嘛干嘛。餐好了,叫号你去取。

21.3.1 CompletableFuture:Java 的异步利器

我个人非常喜欢 CompletableFuture。它把回调地狱变成了链式调用,代码可读性高了不少。

// 重构前:同步调用,串行执行
Order order = orderService.getOrder(orderId);
User user = userService.getUser(order.getUserId());
Product product = productService.getProduct(order.getProductId());
return new OrderDetail(order, user, product);

// 重构后:异步并行,CompletableFuture
CompletableFuture<Order> orderFuture = 
    CompletableFuture.supplyAsync(() -> orderService.getOrder(orderId));
CompletableFuture<User> userFuture = 
    orderFuture.thenCompose(order -> 
        CompletableFuture.supplyAsync(() -> userService.getUser(order.getUserId())));
CompletableFuture<Product> productFuture = 
    orderFuture.thenCompose(order -> 
        CompletableFuture.supplyAsync(() -> productService.getProduct(order.getProductId())));

return orderFuture.thenCombine(userFuture, (order, user) -> 
    orderFuture.thenCombine(productFuture, (o, p) -> 
        new OrderDetail(o, user, p))).join();

你看,三个原本串行的调用,现在可以并行执行了。总耗时从三个接口时间之和,变成了最慢的那个接口时间。

21.3.2 async/await:Kotlin 的协程之美

如果你用 Kotlin,那 async/await 简直是天赐之物。它让异步代码写起来像同步一样自然。

// 重构前:回调嵌套
fun loadData(callback: (Data) -> Unit) {
    api.fetchUser { user ->
        api.fetchPosts(user.id) { posts ->
            callback(Data(user, posts))
        }
    }
}

// 重构后:async/await
suspend fun loadData(): Data {
    val user = async { api.fetchUser() }
    val posts = async { api.fetchPosts(user.await().id) }
    return Data(user.await(), posts.await())
}

关键点:async/await 不是魔法,它本质上是编译器帮你生成了状态机。每个 await 都是一个挂起点,线程不会阻塞,而是去干别的事。

21.4 响应式编程重构:从「拉」到「推」

传统的编程模式是「拉」——你主动去拿数据。响应式编程是「推」——数据来了,它主动通知你。这在处理流式数据、高吞吐场景下,优势非常明显。

21.4.1 从 List 到 Flux

// 重构前:阻塞式集合操作
List<Order> orders = orderService.getAllOrders();
List<OrderDetail> details = orders.stream()
    .map(order -> {
        User user = userService.getUser(order.getUserId()); // 阻塞
        Product product = productService.getProduct(order.getProductId()); // 阻塞
        return new OrderDetail(order, user, product);
    })
    .collect(Collectors.toList());

// 重构后:响应式 Flux
Flux<Order> orderFlux = orderService.getAllOrdersReactive();
Flux<OrderDetail> detailFlux = orderFlux.flatMap(order -> 
    Mono.zip(
        userService.getUserReactive(order.getUserId()),
        productService.getProductReactive(order.getProductId())
    ).map(tuple -> new OrderDetail(order, tuple.getT1(), tuple.getT2()))
);

你发现区别了吗?重构前,每个 getUsergetProduct 都是阻塞的。重构后,它们可以并发执行,而且不阻塞线程。

21.4.2 背压:别把消费者撑死

响应式编程里有个重要概念叫「背压」。说白了就是:生产者太快,消费者跟不上怎么办?

我曾经遇到过一个场景,消息队列里的数据量突然暴增,消费者处理不过来,直接 OOM 了。后来用了背压策略,消费者处理多少,生产者就发多少,问题就解决了。

// 背压示例:每次只处理 10 个
Flux.range(1, 1000)
    .limitRate(10)  // 每次只请求 10 个
    .subscribe(System.out::println);

我的建议:响应式编程的学习曲线确实陡峭,但一旦掌握,处理高并发、高吞吐的场景会非常顺手。先从简单的 Flux/Mono 开始,别一上来就搞复杂的 Operator。

21.5 重构实战:一个完整的案例

最后,我们来看一个完整的重构案例。这是一个用户订单查询服务,重构前是同步阻塞的,重构后变成了异步响应式。

// 重构前:同步阻塞
public class OrderService {
    public OrderDetail getOrderDetail(String orderId) {
        Order order = orderRepo.findById(orderId); // 阻塞
        User user = userRepo.findById(order.getUserId()); // 阻塞
        Product product = productRepo.findById(order.getProductId()); // 阻塞
        return new OrderDetail(order, user, product);
    }
}

// 重构后:异步响应式
public class OrderServiceReactive {
    public Mono<OrderDetail> getOrderDetail(String orderId) {
        return orderRepo.findByIdReactive(orderId)
            .flatMap(order -> 
                Mono.zip(
                    userRepo.findByIdReactive(order.getUserId()),
                    productRepo.findByIdReactive(order.getProductId())
                )
                .map(tuple -> new OrderDetail(order, tuple.getT1(), tuple.getT2()))
            );
    }
}

重构后的代码,不仅性能提升了(三个查询并行),而且资源利用率更高(不阻塞线程)。

避坑指南:我曾经在重构时犯过一个错误——把所有的同步调用都改成了异步,结果线程池被打满,系统反而更慢了。异步不是银弹,它适合 I/O 密集型场景,不适合 CPU 密集型场景。CPU 密集型的任务,用异步反而会增加上下文切换的开销。

并发与异步重构,说到底就是两件事:一是保证数据安全,二是提高资源利用率。前者靠锁和原子类,后者靠异步和响应式。掌握了这些,你的系统在高并发下才能稳如磐石。

并发与异步重构知识体系 线程安全重构 • 不可变对象:final + 只读 • 原子类:AtomicInteger / LongAdder • CAS 机制:无锁并发 • synchronized 优化 锁粒度优化 • 锁细化:只锁关键路径 • 锁粗化:减少加锁次数 • 读写锁:读多写少场景 • 分段锁:ConcurrentHashMap 异步编程模式 • CompletableFuture:链式回调 • async/await:协程挂起 • 线程池管理:避免打满 • 异常处理: exceptionally 响应式编程重构 • Flux/Mono:数据流 • 背压策略:控制流速 • flatMap:并发处理 • 非阻塞 I/O 目标:高性能 + 线程安全 + 高资源利用率

公众号:蓝海资料掘金营,微信deep3321