从面试翻车到原理吃透:详解 synchronized 锁不住 Integer 的真相

上周朋友兴致勃勃地去某大厂面 Java 开发,前半程聊项目、讲架构都很顺利,直到面试官盯着他简历上 "精通 Java 并发编程" 的字样,话锋一转聊起了锁。

"看你项目里多线程用得不少,那我们聊聊实际场景 ------ 如果用 synchronized 锁一个 Integer 对象,能保证线程安全吗?" 面试官推了推眼镜,语气很平和。

朋友后来跟我说,当时他想都没想就自信回答:"当然可以啊!synchronized 是互斥锁,只要锁住同一个对象,就能保证临界区代码的原子性。"

结果面试官没接话,直接在白板上写下了一段代码:

typescript 复制代码
public class IntegerLockProblem {
    private static Integer count = 0;
    
    public void increment() {
        synchronized(count) {
            count++;
        }
    }
}

"现在假设有 10 个线程,每个线程都调用 1000 次 increment () 方法,你觉得最终 count 的值会是 10000 吗?"

朋友说他当时还是没察觉到问题,笃定地回答:"肯定是 10000 啊!synchronized 都锁住 count 了,count++ 的操作不会有线程安全问题。"

直到面试官摇头说 "实际运行结果会随机,可能是 2000,也可能是 3000,永远到不了 10000",他才彻底懵了 ------ 明明用了 synchronized,怎么还会有线程安全问题?

一、为什么 synchronized 锁不住 Integer?核心原因就 3 点

后来朋友回来查资料、翻源码,才彻底搞懂这背后的逻辑,其实问题的关键根本不是 synchronized 的用法,而是对 "Integer 特性" 和 "锁原理" 的理解不到位:

1. 先看清 count++ 的本质:根本不是 "一步操作"

我们直觉里觉得 count++ 很简单,但实际上它拆成了 3 个独立步骤:

  • 第一步:读取当前 count 对象的值(比如此时是 0);
  • 第二步:将读取到的值加 1(变成 1);
  • 第三步:把加 1 后的结果重新赋值给 count 变量。

这三步在多线程环境下如果没有被完全 "保护",就会出现线程安全问题 ------ 但更致命的是下面这一点。

2. Integer 是 "不可变对象":每次 ++ 都会产生新对象

这是最核心的坑!Java 里的 Integer、String、Boolean 这些类都是不可变类,也就是说,一旦创建了对象,它的值就永远不能被修改。

那 count++ 的时候发生了什么?其实底层执行的是这样的逻辑:

ini 复制代码
// 表面上的count++
count = count + 1;
// 实际底层执行(简化版)
count = Integer.valueOf(count.intValue() + 1);

看到了吗?每次执行 count++,都会先把 Integer 对象转成 int 值加 1,再通过 Integer.valueOf () 创建一个新的 Integer 对象,最后把新对象的引用赋值给 count 变量。

3. synchronized 锁的是 "对象实例",不是 "变量名"

朋友最大的误区就在这里:他以为锁住的是 "count 这个变量",但实际上 synchronized 锁的是 "变量指向的对象实例"。

举个具体的例子:

  • 线程 A 进入 increment (),此时 count 指向值为 0 的 Integer 对象,synchronized 锁住这个 "0 对象";
  • 线程 A 执行 count++,创建了值为 1 的新 Integer 对象,count 变量现在指向这个 "1 对象";
  • 此时线程 B 进入 increment (),它看到的 count 指向 "1 对象",所以 synchronized 锁住的是 "1 对象"------ 和线程 A 锁的根本不是同一个对象!

两个线程锁的是不同对象,自然无法保证互斥,count 的值也就会出现混乱。

二、3 种正确解决方案:从原理到适用场景

搞懂了问题根源,解决方法就很清晰了。面试官当时也给出了方向,朋友后来整理了 3 种主流方案,各有适用场景:

方案 1:锁 "类对象"------ 简单但性能差

把 synchronized 的锁对象改成当前类的 Class 对象,代码如下:

typescript 复制代码
public void increment() {
    // 锁类对象,所有线程都会锁同一个对象
    synchronized(IntegerLockProblem.class) {
        count++;
    }
}

原理:每个类在 JVM 中只有一个 Class 对象,不管多少线程,锁的都是同一个对象,能保证互斥。

缺点:类对象的锁粒度太大,如果这个类里还有其他同步方法,会导致所有同步方法都互斥,影响性能。

适用场景:并发量极低,或者类中只有这一个同步方法的简单场景。

方案 2:用 "专门的锁对象"------ 平衡性能和安全性

定义一个 private final 的 Object 对象作为锁,专门用于同步,代码如下:

typescript 复制代码
// 专门的锁对象,用final保证不会被修改引用
private final Object lock = new Object();
public void increment() {
    synchronized(lock) {
        count++;
    }
}

原理:lock 是专门的锁对象,且用 final 修饰(保证不会被重新赋值指向其他对象),所有线程都锁同一个 lock 对象,同时锁粒度只限于 increment () 方法。

优点:性能比锁类对象好,且安全性高,是日常开发中最常用的方案之一。

适用场景:大多数需要自定义同步逻辑的场景,尤其是类中有多个同步方法时,能避免锁竞争。

方案 3:用 AtomicInteger------ 最优解(推荐)

直接用 JUC 包下的 AtomicInteger,它本身就是线程安全的原子类,不需要手动加锁:

csharp 复制代码
// 用AtomicInteger替代Integer,本身支持原子操作
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    // 原子操作,底层用CAS实现,无需手动加锁
    count.incrementAndGet();
}

原理:AtomicInteger 底层通过 "CAS(Compare And Swap)" 机制实现原子操作,不需要 synchronized,性能比同步锁更好(尤其是高并发场景)。

优点:无需手动处理锁,代码更简洁,性能更高,且能避免锁相关的问题(比如死锁、锁粒度问题)。

适用场景:所有需要对整数进行原子操作的场景(如计数、累加),是这 3 种方案中的最优解。

三、面试官真正想考察的:不是 "会不会用锁",而是 "理解有多深"

朋友后来复盘时才明白,面试官问这个问题,根本不是考察 "synchronized 怎么用"------ 那是入门级问题。真正想考察的是 3 个核心能力:

  1. 对 "不可变对象" 的理解深度:是否知道 Integer、String 这些类的不可变特性,以及这种特性在并发场景下的影响;
  1. 对 "锁原理" 的掌握程度:是否清楚 synchronized 锁的是 "对象实例" 而非 "变量",能否识别 "锁对象被修改" 的坑;
  1. 线程安全方案的 "选型能力" :面对问题时,能否根据场景选择最优方案(比如知道 AtomicInteger 比手动加锁更好),而不是只会用一种方法。

这些看似 "基础" 的问题,其实最能暴露真实的技术功底。朋友说,以前总觉得并发编程要学 "高深" 的 AQS、线程池参数调优,却忽略了这些底层基础 ------ 而大厂面试恰恰爱考这些 "看似简单,实则藏坑" 的题。

最后:别让 "基础" 成为面试的绊脚石

朋友这次面试虽然没通过,但这个问题让他彻底清醒了:Java 并发编程的核心,从来不是 "会用多少 API",而是 "理解底层原理"。

比如:

  • 以为 synchronized 能锁住一切,却忽略了 "锁对象是否会变化";
  • 以为 count++ 是原子操作,却不知道它拆成了三步;
  • 以为 Integer 和普通对象一样,却忘了它是不可变类。

这些 "想当然" 的误区,恰恰是面试中的 "杀招"。与其盲目啃高深的知识点,不如先把这些基础原理吃透 ------ 毕竟,能把简单问题讲清楚,比能讲复杂问题更能体现技术深度。

相关推荐
麦麦鸡腿堡3 小时前
Java的封装
java·开发语言
wearegogog1233 小时前
Centos7下docker的jenkins下载并配置jdk与maven
java·docker·jenkins
青云交3 小时前
Java 大视界 -- Java 大数据在智能家居设备联动与场景自动化中的应用
java·大数据·智能家居·数据采集·设备联动·场景自动化·逻辑实现
野犬寒鸦3 小时前
从零起步学习MySQL || 第一章:初识MySQL及深入理解内部数据类型
java·服务器·数据库·后端·mysql
自由的疯3 小时前
java spring blob 附件 下载
java·后端·架构
vir024 小时前
翻转后1的数量(dp)
java·数据结构·算法
Full Stack Developme4 小时前
Python Calendar 模块教程
java·服务器·python
Loch4 小时前
Spring AI 实战:构建一个“懂上下文”的智能对话机器人 (MCP 模式)
java
李贺梖梖4 小时前
Maven 设置项目编码,防止编译打包出现编码错误
java·maven