java synchronized,volatile关键字 说明
要理解 Java 中的 synchronized 和 volatile 关键字,核心是搞懂它们如何解决多线程并发安全问题(比如可见性、原子性、有序性)。我们先从基础概念入手,再结合代码示例对比两者的差异和适用场景。
一、前置知识:并发的三大问题
多线程并发时会遇到 3 个核心问题,这是理解两个关键字的前提:
- 可见性:一个线程修改了共享变量,其他线程能立刻看到修改后的值(CPU 缓存会导致变量值 “不一致”)。
- 原子性:一个操作(或多个操作)要么全部执行且执行过程不被打断,要么都不执行。
- 有序性:程序执行顺序按代码书写顺序执行(JVM 指令重排会打乱顺序)。
二、volatile 关键字
1. 核心作用
volatile 是轻量级的同步机制,仅能修饰成员变量 / 静态变量,解决两个问题:
- ✅ 保证可见性:修改后立刻刷新到主内存,其他线程读取时直接从主内存加载(跳过 CPU 缓存)。
- ✅ 禁止指令重排:避免 JVM 优化导致代码执行顺序错乱(比如单例模式的双重检查锁)。
- ❌ 不保证原子性:多线程对 volatile 变量的复合操作(如 i++)仍会线程不安全。
2. 代码示例(volatile 的可见性 + 原子性问题)
public class VolatileDemo {
// 用volatile修饰共享变量
private static volatile boolean flag = false;
// 测试原子性的变量
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
// 测试1:可见性
new Thread(() -> {
while (!flag) {
// 循环直到flag变为true
}
System.out.println("线程1:检测到flag已修改");
}).start();
// 主线程延迟1秒后修改flag
Thread.sleep(1000);
flag = true;
System.out.println("主线程:已将flag设为true");
// 测试2:原子性问题(count++ 非原子操作)
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
count++; // 复合操作:读取→加1→写入,volatile无法保证原子性
}
}).start();
}
// 等待所有线程执行完毕
Thread.sleep(2000);
System.out.println("最终count值:" + count); // 预期100000,实际小于100000
}
}
结果解释:
可见性:线程 1 能立刻感知到 flag 的修改,循环终止(如果不加 volatile,线程 1 会一直循环)。
原子性:count++ 最终值远小于 100000,因为多线程同时读写导致数据覆盖。
3. volatile 典型使用场景
状态标记位:如示例中的 flag,用于控制线程启动 / 停止。
单例模式(双重检查锁):禁止指令重排,避免创建半初始化对象:
public class Singleton {
// volatile 禁止指令重排
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 不加volatile可能导致指令重排
}
}
}
return instance;
}
}
三、synchronized 关键字
1. 核心作用
synchronized 是重量级锁(JDK 1.6 后优化了性能),解决可见性、原子性、有序性全部三个问题:
- ✅ 保证原子性:加锁后,同一时间只有一个线程能执行锁定的代码块。
- ✅ 保证可见性:释放锁时会将修改的变量刷新到主内存,获取锁时从主内存加载变量。
- ✅ 保证有序性:加锁后禁止指令重排。
2. 使用方式(3 种)
表格
使用方式 锁定对象 示例
修饰实例方法 当前实例对象(this) public synchronized void method()
修饰静态方法 当前类的 Class 对象 public static synchronized void method()
修饰代码块 自定义对象(任意对象) synchronized (lockObj) { … }
3. 代码示例(解决 volatile 的原子性问题)
public class SynchronizedDemo {
private static int count = 0;
// 自定义锁对象(推荐:避免用this/Class对象,降低锁竞争)
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 1000个线程,每个线程执行100次count++
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
// 方式1:同步代码块(推荐)
synchronized (lock) {
count++; // 加锁后,复合操作变为原子性
}
// 方式2:同步静态方法(等价于synchronized (SynchronizedDemo.class))
// increment();
}
}).start();
}
Thread.sleep(2000);
System.out.println("最终count值:" + count); // 精准100000
}
// 同步静态方法
public static synchronized void increment() {
count++;
}
}
结果解释:加锁后 count++ 变为原子操作,最终值精准等于 100000,解决了并发安全问题。
四、synchronized vs volatile 核心对比
| 特性 | synchronized | volatile |
|---|---|---|
| 修饰范围 | 方法、代码块 | 成员变量、静态变量 |
| 解决问题 | 可见性、原子性、有序性 | 可见性、有序性(无原子性) |
| 性能 | 重量级(JDK1.6 后优化) | 轻量级(无锁开销) |
| 锁特性 | 互斥锁(独占) | 无锁(仅内存语义) |
| 适用场景 | 复合操作(如 i++、多步逻辑) | 简单状态标记、禁止指令重排 |
五、关键注意事项
synchronized 锁优化:JDK 1.6 引入了偏向锁、轻量级锁、重量级锁的升级机制,大幅降低了锁的开销,不必过度担心性能问题。
volatile 不能替代 synchronized:仅当操作是单一读 / 写(如 flag = true)时用 volatile;涉及复合操作(如 i++、多步修改)必须用 synchronized 或原子类(AtomicInteger)。
锁的粒度:尽量缩小 synchronized 代码块的范围(仅包裹核心并发逻辑),减少锁竞争。
总结
核心能力:volatile 是轻量级同步,保证可见性和有序性但无原子性;synchronized 是重量级同步,解决所有并发三大问题。
选型原则:简单状态标记用 volatile,复合操作 / 多步逻辑用 synchronized。
性能权衡:volatile 无锁开销,适合高并发读、低并发写的场景;synchronized 有锁开销,但能保证完全的并发安全。
更多推荐




所有评论(0)