前面我们聊了 Java 并发基础,比如进程、线程、线程状态、synchronizedvolatile 的基本区别。

这一篇继续往下走,进入 Java 并发里更核心的一组内容:

  • JMM 是什么?
  • 主内存和工作内存是什么?
  • volatile 为什么能保证可见性?
  • 什么是指令重排序?
  • happens-before 是什么?
  • CAS 是什么?
  • ABA 问题怎么理解?
  • AtomicIntegervolatile int 有什么区别?
  • LongAdder 为什么高并发下更快?
  • synchronized 锁升级是什么?
  • ReentrantLocksynchronized 有什么区别?

这些都是 Java 后端面试里非常高频的并发问题。


一、什么是 JMM?

JMM 全称是 Java Memory Model,也就是 Java 内存模型。

注意,它不是 JVM 内存结构里的堆、栈、方法区。

JMM 是一套并发规范,用来规定:

线程如何读写共享变量
线程之间如何保证可见性
哪些操作不能随便重排序

简单理解:

JMM 主要解决多线程下的可见性和有序性问题。

二、主内存和工作内存是什么?

JMM 抽象出两个概念:

概念 含义
主内存 共享变量实际存放的地方
工作内存 每个线程自己的变量副本

图示:

线程操作共享变量时,通常会先从主内存拷贝到自己的工作内存,再进行操作。

这就可能导致一个问题:

一个线程修改了变量,另一个线程不一定马上看到。

三、为什么会有可见性问题?

比如有一个变量:

boolean flag = true;

线程 A 执行:

flag = false;

线程 B 执行:

while (flag) {
    // do something
}

如果没有 volatile 或锁,线程 B 可能一直读自己工作内存中的旧值 true,看不到线程 A 已经把它改成了 false

这就是可见性问题。


四、volatile 如何保证可见性?

使用 volatile 修饰变量:

private volatile boolean flag = true;

它的效果是:

线程写 volatile 变量时,会把值刷新到主内存;
线程读 volatile 变量时,会从主内存读取最新值。

所以一个线程修改后,其他线程能及时看到。

面试简洁说法:

volatile 通过内存屏障保证变量修改对其他线程可见。

五、什么是指令重排序?

为了提高性能,编译器和 CPU 可能会调整指令执行顺序。

比如代码顺序是:

1. a = 1
2. b = 2

实际执行时,只要不影响单线程结果,可能变成:

1. b = 2
2. a = 1

单线程看起来没问题,但多线程下可能出错。

所以并发里不仅要关注可见性,还要关注有序性。


六、volatile 如何禁止指令重排序?

volatile 会插入内存屏障,限制特定指令前后不能随意重排。

最经典的场景是单例模式的双重检查锁定:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

如果没有 volatile,对象创建过程可能发生重排序,其他线程可能拿到一个“还没初始化完成”的对象。

所以 DCL 单例里,instance 通常要加 volatile


七、什么是 happens-before?

happens-before 是 JMM 中判断可见性的规则。

如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见。

常见规则:

规则 含义
程序顺序规则 同一线程内,前面的操作先于后面的操作
volatile 规则 写 volatile 先于后续读 volatile
锁规则 解锁先于后续加锁
线程启动规则 start() 先于线程内操作
线程终止规则 线程内操作先于 join() 返回

面试不一定要把所有规则背得很细,但要知道:

happens-before 用来描述多线程之间的可见性关系。

八、什么是 CAS?

CAS 全称是 Compare And Swap,比较并交换。

它是一种乐观锁思想。

CAS 有三个值:

内存值 V
期望值 A
新值 B

逻辑是:

如果 V == A,就把 V 改成 B;
否则更新失败。

可以这样理解:

我以为 count 还是 5;
如果它真是 5,我就改成 6;
如果不是 5,说明别人改过了,我就失败或重试。

九、CAS 为什么是原子的?

CAS 底层依赖 CPU 原子指令。

Java 中很多 CAS 操作通过 UnsafeVarHandle 实现,最终依赖硬件保证“比较并交换”这一步不可被打断。

所以 CAS 不需要像 synchronized 那样阻塞线程,属于无锁并发的一种基础。


十、CAS 有什么问题?

CAS 常见问题有三个:

问题 说明
ABA 问题 值从 A 变 B 又变回 A,CAS 以为没变
自旋开销 失败后不断重试,竞争激烈时浪费 CPU
只能保证单变量原子更新 多个变量一致更新比较复杂

自旋开销很好理解:

CAS 失败 -> 重试
又失败 -> 再重试
竞争越激烈,CPU 空转越明显

所以 CAS 适合竞争不是特别激烈、逻辑比较短的场景。


十一、什么是 ABA 问题?

CAS 只检查值有没有变成期望值。

比如线程 A 看到变量是:

A

然后线程 B 把它改成:

A -> B -> A

线程 A 再执行 CAS 时发现还是 A,就以为没变过,于是更新成功。

但实际上它中间被改过。

这就是 ABA 问题。

解决思路:

加版本号

例如:

A, version = 1
B, version = 2
A, version = 3

虽然值又回到了 A,但版本号变了,CAS 就能发现中间发生过修改。


十二、AtomicInteger 底层是什么?

AtomicInteger 底层主要基于 CAS。

常见方法:

incrementAndGet()
getAndIncrement()
compareAndSet()

示例:

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

它可以保证自增操作的原子性。

比下面这种更安全:

volatile int count = 0;
count++;

十三、AtomicInteger 和 volatile int 有什么区别?

对比 volatile int AtomicInteger
可见性 保证 保证
原子性 不保证 count++ 原子性 保证原子更新
底层机制 内存屏障 CAS
适合场景 状态标记 原子计数

所以计数器场景应该用:

AtomicInteger

而不是只用 volatile


十四、LongAdder 是什么?

LongAdder 也是计数器,但它在高并发下通常比 AtomicLong 性能更好。

原因是它会把热点计数拆散到多个 Cell 上:

base + Cell1 + Cell2 + Cell3 ...

多个线程可以分散更新不同 Cell,减少 CAS 竞争。

取值时再汇总。

适合高并发统计,比如:

QPS
请求次数
接口访问量

十五、synchronized 锁升级是什么?

为了优化性能,JVM 对 synchronized 做了锁升级。

经典过程:

无锁
  ↓
偏向锁
  ↓
轻量级锁
  ↓
重量级锁

意思是:

根据竞争程度逐步升级,而不是一开始就使用重量级操作系统互斥量。

不过要注意,不同 JDK 版本对偏向锁支持有变化,面试掌握思想即可。


十六、轻量级锁和重量级锁有什么区别?

轻量级锁主要通过 CAS 尝试获取锁,适合竞争不激烈的场景。

重量级锁会让线程阻塞和唤醒,涉及操作系统调度,开销更大。

可以简单记:

竞争少:CAS 自旋,轻量
竞争激烈:线程阻塞,重量

所以 synchronized 不是一上来就很重,它会根据竞争情况进行优化。


十七、ReentrantLock 是什么?

ReentrantLock 是 JUC 包下的可重入锁。

示例:

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

一定要在 finally 中释放锁,防止异常导致锁不释放。


十八、ReentrantLock 和 synchronized 有什么区别?

对比 synchronized ReentrantLock
类型 JVM 内置关键字 JUC 工具类
释放锁 自动释放 手动 unlock
可中断 不方便 支持 lockInterruptibly
尝试加锁 不支持 支持 tryLock
公平锁 不支持指定 支持公平/非公平
条件队列 一个 wait set 多个 Condition

简单说:

synchronized 简单易用,ReentrantLock 更灵活。

十九、什么是可重入锁?

可重入锁指同一个线程拿到锁后,可以再次获得同一把锁。

例如:

synchronized void a() {
    b();
}

synchronized void b() {
}

同一个线程进入 a() 后已经拿到了锁,再调用 b() 时可以继续进入,不会把自己锁死。

synchronizedReentrantLock 都是可重入锁。


二十、公平锁和非公平锁有什么区别?

公平锁:

线程按排队顺序获取锁

非公平锁:

新来的线程可以尝试插队抢锁

ReentrantLock 默认是非公平锁:

new ReentrantLock();      // 非公平锁
new ReentrantLock(true);  // 公平锁

非公平锁吞吐量通常更高,因为减少线程切换。

公平锁更“按顺序”,但性能可能差一些。


二十一、这一组怎么串起来讲?

可以这样回答:

JMM 是 Java 内存模型,主要解决多线程下共享变量的可见性和有序性问题。
线程操作共享变量时,可能先读到自己的工作内存,导致其他线程修改不可见。
volatile 通过内存屏障保证可见性,并禁止特定指令重排序。

CAS 是比较并交换,属于乐观锁思想,底层依赖 CPU 原子指令。
AtomicInteger 基于 CAS,能保证自增原子性,但 CAS 有 ABA、自旋开销和单变量限制。
LongAdder 通过分段计数降低高并发下的 CAS 竞争。

synchronized 有锁升级优化,不是一开始就重量级。
ReentrantLock 比 synchronized 更灵活,支持 tryLock、可中断、公平锁和多个 Condition。

总体流程图

总结

这一组可以按下面这条线来记:

JMM 解决多线程下可见性和有序性问题。
线程操作共享变量可能先读到工作内存,导致其他线程修改不可见。
volatile 通过内存屏障保证可见性,并禁止特定重排序。

happens-before 用来描述操作之间的可见性关系。
CAS 是比较并交换,属于乐观锁思想,底层依赖 CPU 原子指令。
AtomicInteger 基于 CAS,能保证自增原子性。
CAS 有 ABA、自旋开销、只能更新单变量等问题。

LongAdder 通过分段计数降低高并发竞争。
synchronized 有锁升级优化,不是一开始就重量级。
ReentrantLock 比 synchronized 更灵活,支持 tryLock、可中断、公平锁和多个 Condition。

这一组重点背:JMM、主内存/工作内存、volatile 内存屏障、happens-before、CAS、ABA、AtomicIntegerLongAdder、锁升级、ReentrantLock vs synchronized、公平锁/非公平锁。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐