You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

由线程安全类构成的程序是否必然线程安全?例外场景有哪些?

完全由线程安全类组成的程序,未必是线程安全的

这是个很容易踩的误区——不少开发者觉得只要代码里用的全是线程安全类,整个程序就自动具备线程安全性了,但实际远不是这么回事。线程安全类只能保证自身内部状态在多线程调用下的一致性,但程序的线程安全是全局属性,取决于这些类如何被组合、调用,以及外部业务逻辑的同步策略。

下面举几个典型场景,说明即使全用线程安全类,程序依然可能出现线程安全问题:

1. 组合操作缺乏原子性

线程安全类的单个方法是原子的,但如果把多个方法调用组合成一个业务逻辑,这个组合操作本身未必是原子的。比如用ConcurrentHashMapAtomicInteger实现"不存在则添加并计数"的逻辑:

private final ConcurrentHashMap<String, String> safeMap = new ConcurrentHashMap<>();
private final AtomicInteger safeCounter = new AtomicInteger(0);

// 这个方法不是线程安全的!
public void addIfMissing(String key, String value) {
    if (!safeMap.containsKey(key)) { // 单个方法线程安全
        safeMap.put(key, value);     // 单个方法线程安全
        safeCounter.incrementAndGet(); // 单个方法线程安全
    }
}

两个线程同时调用这个方法时,可能都通过了containsKey的判断,然后都执行putincrement,导致同一个key被插入两次,计数器多增一次——因为这三个操作的组合没有被原子化。

2. 跨类的状态依赖未同步

如果多个线程安全类之间存在业务上的依赖关系,仅仅调用各自的线程安全方法不足以保证整体一致性。比如订单类SafeOrder和库存类SafeInventory都是线程安全的,但创建订单时需要扣减库存:

// 假设这两个都是线程安全类
private final SafeOrder orderService = new SafeOrder();
private final SafeInventory inventoryService = new SafeInventory();

// 这个逻辑不是线程安全的
public void createOrder(String itemId, int quantity) {
    orderService.createNewOrder(itemId, quantity);
    inventoryService.deductStock(itemId, quantity);
}

如果线程A执行完创建订单,还没扣减库存时,线程B查询库存,会得到未扣减的旧值,导致库存超卖;或者如果扣减库存失败,但订单已经创建,就会出现数据不一致。这种跨类的状态变更需要用事务、同步块或者其他协调机制来保证原子性。

3. 线程安全类的不当使用

有些线程安全类是"有条件的线程安全",如果不遵循其使用规范,依然会出问题。比如:

  • Collections.synchronizedList是线程安全的,但它的迭代器不是线程安全的——如果在迭代时其他线程修改了列表,可能会抛出ConcurrentModificationException,除非你在迭代时手动给列表加锁。
  • ConcurrentHashMap的迭代器是fail-safe的,但它只能保证迭代过程中不会抛出异常,不能保证迭代时看到的是某个时刻的快照。如果你的业务逻辑依赖于"迭代期间数据不变化",那这种情况就会破坏线程安全的预期。

总结

线程安全不是靠单个类的线程安全属性简单叠加出来的,它需要考虑整个程序的执行逻辑、状态流转的原子性,以及线程安全类的正确使用方式。要构建真正线程安全的程序,除了选择合适的线程安全类,还要关注类之间的交互边界,必要时通过同步锁、原子类、并发工具类来保证复合操作的原子性和一致性。

内容的提问来源于stack exchange,提问作者Farruh Habibullaev

火山引擎 最新活动