面试官:怎么看项目代码会不会产生并发问题,有没有一个范式去进行检查,提前预防

40 阅读4分钟

面试官您好,关于并发问题的识别和预防,我想结合我在项目中的经验,和您分享一下我的看法和实践方法。

一、从一个真实的“并发事故”说起

在我之前参与的一个外卖项目(苍穹外卖)中,就遇到过一个典型的并发问题。当时我们的库存扣减逻辑是这样的:

if (product.getStock() > 0) {
    product.setStock(product.getStock() - 1);
    // 更新数据库
}

看起来没什么问题,对吧?但在一次大促活动中,我们发现库存经常被扣成负数。经过排查,发现是多个用户同时下单时,都读取到了相同的库存值,然后都通过了if判断,导致超卖。

这个经历让我深刻认识到,并发问题如果不提前预防,一旦在生产环境爆发,后果是非常严重的。

二、我是怎么检查代码有没有并发风险的?

现在,每当接手一个新模块或者review代码时,我都会按这三个步骤来“扫描”一遍,就像医生给病人做体检一样。

第一步:找“大家都能碰的东西”

并发问题的前提是有多个人(线程)能接触到同一个东西(数据),而且这个东西还能被修改。

我会特别留意这几种情况:

1.静态变量:比如一个类里的public static User currentUser;,所有地方都能访问和修改,风险很高。

2.单例模式的对象:全局只有一个实例,它的成员变量也是大家共用的。

  1. 传来传去的对象:比如把一个订单对象放到任务队列里,多个线程都可能去修改它。

第二步:看“操作安不安全”

找到了共享的数据,就要看对它的操作会不会出问题:

  • 危险操作(需要特别小心):

    • 自增自减:比如i++,看起来是一步,其实是三步(读取、加1、写回),中间可能被打断。
    • 先检查再操作:比如先看购物车里有没有某件商品,有就删掉,但可能在你检查完和删除之间,别人已经删了。
  • 安全操作(可以放心用):

    • 只读取不修改:比如只是查看某个值。
    • 使用专门设计的“安全工具”:比如Java里的ConcurrentHashMap、AtomicInteger等。

第三步:检查“锁”有没有用对

如果代码里用了锁(比如synchronized),还要检查:

  • 锁的对象对不对:不能每次new一个新对象来当锁,这样每个线程锁的都是不同的东西,等于没锁。
  • 锁的范围合不合适:锁得太宽影响性能,锁得太窄又保护不了数据。
  • 复合操作的原子性:比如AtomicInteger能保证单个操作安全,但像“先检查再减1”这样的组合操作,还是需要额外保护。

三、我的几个“防并发”小技巧

在日常开发中,我也总结了一些实用的技巧,可以有效避免并发问题:

1. 能不共享,就不共享

尽量让每个线程都有自己的“工作区”,不要去碰别人的东西。比如:

// 不好:多个线程可能同时修改temp
public class BadExample {
    private String temp;
    public void process() {
        temp = "xxx"// 可能被其他线程覆盖
        // ...
    }
}
// 好:每个线程都有自己的temp
public class GoodExample {
    public void process() {
        String temp = "xxx"// 只在当前方法里使用
        // ...
    }
}

2. 用专门的“安全工具”

Java为我们提供了很多线程安全的工具类,用它们来替代不安全的旧工具:

不安全的工具推荐的安全替代品
HashMapConcurrentHashMap
ArrayListCopyOnWriteArrayList
SimpleDateFormatDateTimeFormatter
i++AtomicInteger.incrementAndGet()

3. 合理使用锁

使用锁时,尽量只锁真正需要保护的那几行代码,不要锁住整个方法。比如:

public void updateOrder(Order order) {
    synchronized(order) { // 只锁住订单对象
        // 修改订单状态的核心代码
    }
    // 其他不涉及共享数据的操作
}

4. 写点测试代码试试

我会写个简单的多线程程序来验证我的代码是不是安全的:

// 创建10个小工
ExecutorService executor = Executors.newFixedThreadPool
(10);
// 用一个安全的计数器
AtomicInteger counter = new AtomicInteger(0);
// 让100个小工都来给计数器加1
for (int i = 0; i < 100; i++) {
    executor.submit(() -> {
        counter.incrementAndGet();
    });
}
// 等所有小工都干完活
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
// 看看结果是不是100
System.out.println(counter.get()); 

四、总结一下

面试官,我觉得并发问题虽然看起来复杂,但核心就一条:多个线程同时修改同一份数据

我的经验是,通过“三步检查法”:

  1. 找到共享的、可变的数据
  2. 检查对这些数据的操作是否安全
  3. 验证同步措施(如锁)是否有效

再配合这几个技巧:

  • 减少共享数据
  • 使用安全的工具类
  • 合理加锁
  • 多写测试代码