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()))
);
你发现区别了吗?重构前,每个 getUser 和 getProduct 都是阻塞的。重构后,它们可以并发执行,而且不阻塞线程。
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 密集型的任务,用异步反而会增加上下文切换的开销。
并发与异步重构,说到底就是两件事:一是保证数据安全,二是提高资源利用率。前者靠锁和原子类,后者靠异步和响应式。掌握了这些,你的系统在高并发下才能稳如磐石。
公众号:蓝海资料掘金营,微信deep3321