文章目录
- [Java 中的 `volatile` 与锁机制详解](#Java 中的
volatile与锁机制详解) -
- [1. `volatile` 是什么?](#1.
volatile是什么?) - [2. `volatile` 解决的两个核心问题](#2.
volatile解决的两个核心问题) -
- [2.1 可见性(Visibility)](#2.1 可见性(Visibility))
- [2.2 有序性(避免部分重排序)](#2.2 有序性(避免部分重排序))
- [3. `volatile` 做不到什么?(常见误区)](#3.
volatile做不到什么?(常见误区)) -
- [3.1 不保证复合操作的原子性](#3.1 不保证复合操作的原子性)
- [3.2 不是锁,不提供互斥](#3.2 不是锁,不提供互斥)
- [4. 典型使用场景](#4. 典型使用场景)
-
- [4.1 线程停止标志/状态控制](#4.1 线程停止标志/状态控制)
- [4.2 配置信息 / 对象引用的热更新](#4.2 配置信息 / 对象引用的热更新)
- [4.3 Double-Checked Locking 单例的 `instance`](#4.3 Double-Checked Locking 单例的
instance)
- [5. Java 内存模型(JMM)中的 `volatile`](#5. Java 内存模型(JMM)中的
volatile) - [6. 对比:`volatile` / `synchronized` / `AtomicXXX`](#6. 对比:
volatile/synchronized/AtomicXXX) - [7. 面试速记结论](#7. 面试速记结论)
- [1. `volatile` 是什么?](#1.
- [`synchronized` vs `ReentrantLock` 对比](#
synchronizedvsReentrantLock对比) -
- [1. 基本用法和特点](#1. 基本用法和特点)
- [2. 可中断](#2. 可中断)
- [3. 超时](#3. 超时)
- [4. 公平性](#4. 公平性)
- [5. Condition 多条件队列](#5. Condition 多条件队列)
- [6. 其他差异](#6. 其他差异)
- [7. 总结对比表](#7. 总结对比表)
- [8. 面试归纳话术](#8. 面试归纳话术)
Java 中的 volatile 与锁机制详解
1. volatile 是什么?
在 Java 中,volatile 是一种轻量级同步机制 ,用来保证变量的可见性 和禁止指令重排序的一部分行为 ,但不保证复合操作的原子性。
声明方式示例:
java
volatile int flag;
volatile boolean running;
volatile Object ref;
2. volatile 解决的两个核心问题
2.1 可见性(Visibility)
多线程下,每个线程有自己的工作内存(寄存器、CPU 缓存等),对共享变量的修改可能只在本线程可见。
使用 volatile 后:
- 一个线程对
volatile变量的写 ,会立刻刷新到主内存。 - 其他线程读同一个
volatile变量时,会直接从主内存读取最新值。
示例:线程退出标志
java
class Worker implements Runnable {
private volatile boolean running = true;
@Override
public void run() {
while (running) {
// do work
}
System.out.println("Stopped.");
}
public void stop() {
running = false;
}
}
如果没有 volatile,某些情况下 while (running) 可能一直读到缓存中的旧值,导致线程无法退出。
2.2 有序性(避免部分重排序)
Java 内存模型(JMM)允许编译器和 CPU 对指令进行重排序,可能导致线程 B 看到线程 A 的某些操作乱序。
- 对
volatile变量的写 :之前对共享变量的写不会被重排序到这次写之后(写屏障)。 - 对
volatile变量的读 :之后的读/写操作不会被重排序到这次读之前(读屏障)。
happens-before 关系:
线程 A 对某个
volatile变量的写 happens-before 线程 B 之后对这个变量的读。
3. volatile 做不到什么?(常见误区)
3.1 不保证复合操作的原子性
像 i++,本质是"读 -> 加一 -> 写",volatile 只保证每次都去主内存读/写,不保证操作整体不可分割。
错误示例:
java
volatile int counter = 0;
public void inc() {
counter++; // 非原子操作
}
多个线程并发调用时,counter 仍会丢失更新,不是线程安全。
正确做法:
- 用原子类
- 或加锁
java
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private final AtomicInteger counter = new AtomicInteger(0);
public void inc() {
counter.incrementAndGet();
}
}
3.2 不是锁,不提供互斥
volatile 只保证可见性,不保证同一时刻只有一个线程操作。需要互斥时还是要用锁:
synchronizedReentrantLock
4. 典型使用场景
4.1 线程停止标志/状态控制
适用于一个线程写标志,多线程读,不涉及复合操作。
java
class Task implements Runnable {
private volatile boolean running = true;
@Override
public void run() {
while (running) {
// do something
}
}
public void shutdown() {
running = false;
}
}
实际项目更推荐
Thread.interrupt(),但volatile boolean仍常见。
4.2 配置信息 / 对象引用的热更新
用于保证对象引用被替换的可见性:
java
class ConfigManager {
private volatile Config config = loadFromFile();
public Config getConfig() {
return config;
}
public void reload() {
this.config = loadFromFile();
}
}
4.3 Double-Checked Locking 单例的 instance
必须加 volatile,否则可能返回构造未完成的对象。
java
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;
}
}
5. Java 内存模型(JMM)中的 volatile
- 写
volatile前的所有写操作"先行发生于"这次volatile写。 - 若线程 B 读取到线程 A 写入的
volatile新值,也能看到 A 在此之前对其他共享变量的修改。
6. 对比:volatile / synchronized / AtomicXXX
| 特性 | volatile |
synchronized |
AtomicXXX |
|---|---|---|---|
| 可见性 | 有 | 有 | 有 |
| 原子性 | 单步读/写有,复合无 | 有 | 具体方法有(如自增) |
| 互斥 | 无 | 有 | 无(CAS自旋) |
| 重排序约束 | 有 | 更强 | 有 |
| 阻塞 | 不会 | 可能(线程挂起) | 一般不会 |
| 典型场景 | 状态标志、热更新指针 | 临界区、多步逻辑 | 计数器、自增、自减 |
7. 面试速记结论
volatile只保证可见性/有序性,不保证原子性。大家常用它做状态标志、配置热更新、DCL 单例指针等。- 多步逻辑或需要原子性的操作就要用
synchronized或AtomicInteger。 - 记住 DCL 单例必须加
volatile,否则会踩坑(构造重排序)。
synchronized vs ReentrantLock 对比
1. 基本用法和特点
一、synchronized:
- 关键字,自动加解锁
- 锁对象是任意 Object
- 语法简单,不易死锁
java
synchronized (lock) {
// 临界区
}
二、ReentrantLock:
- 类,需手动 lock/unlock
- 支持可中断、超时、公平锁、多条件队列等高级功能
java
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
2. 可中断
synchronized等待锁不可中断,即使线程被 interrupt,只是设置标志,不会抛异常或停止等待。ReentrantLock.lockInterruptibly()可中断 ,等待锁时收到 interrupt 会抛InterruptedException,线程可以及时退出。
3. 超时
synchronized不支持超时,只能无限等待。ReentrantLock.tryLock(timeout, unit)支持超时+可中断,时间内没拿到锁返回 false,期间可被 interrupt。
4. 公平性
synchronized语义上不保证公平(老线程可能被饿死)。ReentrantLock构造参数可指定公平锁 (new ReentrantLock(true)),尽量按请求顺序获取锁。
5. Condition 多条件队列
synchronized每把锁只有一个隐式等待队列wait/notify。ReentrantLock可通过newCondition()创建多个条件队列,每队唤醒/等待单独管理,复杂场景易用。
6. 其他差异
- 释放锁:
synchronized自动、异常时也会释放;ReentrantLock必须手动 unlock(常用 finally)。 - 可重入性:两者都支持。
- 调试/监控:
ReentrantLock有状态查询方法(如 isLocked),synchronized没有直接 API。 - 性能:JDK1.6 后
synchronized性能很好,ReentrantLock高并发/复杂要求下更灵活。
7. 总结对比表
| 维度 | synchronized |
ReentrantLock |
|---|---|---|
| 类型 | 语言关键字 (JVM级) | 类 (java.util.concurrent.locks) |
| 获取/释放方式 | 自动 | 手动 lock()/unlock() |
| 可重入 | 是 | 是 |
| 可中断获取锁 | 否 | 是 (lockInterruptibly) |
| 超时获取锁 | 否 | 是 (tryLock(timeout)) |
| 公平锁 | 不保证 | 支持构造参数 |
| 多条件队列 | 不支持 | 支持 (Condition) |
| 性能 | 简单场景足够 | 高并发/复杂需求更强 |
8. 面试归纳话术
问:
synchronized和ReentrantLock如何选择?
- 普通同步优先
synchronized,语法简单安全。 - 需要可中断/超时/公平锁/多条件队列/复杂锁逻辑 时,用
ReentrantLock更佳。 - 对于最细致的定制场景(如限时/可取消业务)优选
ReentrantLock。