面试被问Synchornized和Volatile?当我回答这些以后,面试官端着瑞幸的手颤抖了......
不想被拷打?带你速通 Synchronized 和Volatile核心考点
彦祖亦菲们面试的时候经常被问到 “Synchronized 和Volatile有啥区别”、“Volatile为啥不保证原子性” 等等,是不是总卡壳?别慌!这俩 Java 并发里的 “基础王”,看似简单却藏着高频考点,今天用几分钟帮你捋清核心,下次再被问直接 “反拷打”!
首先我们要先弄清楚:Java为啥需要这俩关键字?
在Java多线程中,影响线程安全的三大"刺客"就是:原子性、可见性和有序性。当多线程访问操作我们的共享变量时,这三者有一个没有处理好就会引起一系列的麻烦。而我们今天要讲的 Synchronized 和Volatile,它们本质上都是帮我们解决这些问题的"良药",但它们之间也有着各自的优缺点和不同。
Synchronized,“全能锁” 但不笨重
Synchronized被称为 “内置锁”,是一种内置的同步机制,用来解决多线程访问共享资源时的线程安全问题。它的主要作用是确保同一时刻只有一个线程能够访问某些特定的代码片段或方法。
Synchronized最核心的能力是同时保证原子性、可见性、有序性,是并发里的 “万能选手”
原子性:一旦某个线程拿到锁,其他线程就得等着,直到它释放锁。这意味着被 Synchronized 修饰的代码块或方法,会被 “打包” 成一个不可分割的操作(比如多个步骤的计算、赋值,不会被其他线程打断)。举个例子:synchronized void add() { count++; },即使 有10 个线程同时调用,count++(拆成 “读 - 改 - 写” 三步)也不会被拆分,最终结果一定正确。下面代码通过两线程各累加1000次的对比,显示不加锁的addWithoutSync()
结果易错,加synchronized
的addWithSync()
结果必为2000,印证其原子性保障。
public class SynchronizedDemo {
// 共享变量:记录累加结果
private static int count = 0;
// 循环次数:让每个线程累加1000次
private static final int LOOP = 1000;
public static void main(String[] args) throws InterruptedException {
// 创建两个线程,都执行累加操作
Thread thread1 = new Thread(() -> {
for (int i = 0; i < LOOP; i++) {
// 不加锁的累加(可能出问题)
// addWithoutSync();
// 加锁的累加(保证正确)
addWithSync();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < LOOP; i++) {
// 不加锁的累加(可能出问题)
// addWithoutSync();
// 加锁的累加(保证正确)
addWithSync();
}
});
// 启动线程
thread1.start();
thread2.start();
// 等待两个线程都执行完
thread1.join();
thread2.join();
// 打印最终结果(预期2000)
System.out.println("最终count值:" + count);
}
// 不加锁的累加方法
private static void addWithoutSync() {
count++;
}
// 加Synchronized的累加方法
private static synchronized void addWithSync() {
count++;
}
}
这个例子能明显看出:Synchronized通过 “独占锁” 的机制,把多步操作 “打包” 成不可分割的原子操作,从而解决了多线程并发时的 “数据错乱” 问题。
可见性:线程释放锁时,会把本地内存里的变量 “同步” 到主内存;其他线程获取锁时,会从主内存重新读取变量。相当于强制刷新了共享变量的 “最新值”,避免线程拿旧数据干活。
有序性:Synchronized 会隐式禁止指令重排序(编译器 / CPU 为了提速,可能打乱代码执行顺序,但保证结果不变;但并发时可能出问题)。被它修饰的代码块,会按 “代码顺序” 执行,不用怕指令乱序导致的逻辑错。
用法:锁对象还是锁类?
Synchronized 的 “锁” 分两种,用的时候别搞混:
-
对象锁:修饰普通方法(锁的是当前对象实例)、修饰代码块(
synchronized(obj) { ... }
,锁的是括号里的 obj 对象)。 比如两个线程用同一个对象调用加锁的普通方法,会互斥;但用不同对象调用,各走各的,不互斥。 -
类锁:修饰静态方法(锁的是类的 Class 对象)、修饰代码块时锁 “类名.class”(
synchronized(A.class) { ... }
)。 整个类只有一个 Class 对象,所以不管用哪个实例调用,只要锁的是类锁,线程都会互斥。
冷知识:Synchronized 早不是 “重量级锁” 了!
很多人觉得 Synchronized “笨重”,其实 JVM 早就对它进行了优化!它会按 “竞争激烈程度” 自动切换锁状态,从 “轻” 到 “重”:
-
偏向锁:无竞争时,谁先拿锁就 “偏向” 谁,下次直接用,不用再申请(省资源)。
-
轻量级锁:轻微竞争时,用 CAS( Compare - and - Swap,比较并交换)自旋尝试拿锁,不用阻塞线程。
-
重量级锁:竞争激烈时,才会升级成传统的 “阻塞锁”(依赖操作系统,开销大)。
Volatile,“轻量标记” 但有短板
核心作用:保可见保有序,唯独缺原子性
Volatile是 “轻量级同步” 关键字,只能修饰变量,能力比 Synchronized 弱:保证可见性、有序性,但不保证原子性。
-
可见性:和 Synchronized 类似,Volatile变量被修改后,会立刻 “刷” 到主内存;其他线程读的时候,会直接从主内存拿最新值,不会用本地缓存的旧值。 比如
volatileboolean flag = false;
,线程 A 改flag=true
,线程 B 能立刻读到 “true”,不会一直卡在 “false” 的死循环里。 -
有序性:通过 “内存屏障” 禁止指令重排序。比如修饰单例模式的
instance
时,能避免 “对象还没初始化完,就被其他线程拿到半成品” 的问题经典的 “双重检查锁” 里必须用Volatile
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不加volatile,此处可能发生指令重排序
instance = new Singleton();
}
}
}
return instance;
}
}
-
不保证原子性:这是高频坑!Volatile只能保证 “读、写单个变量” 是原子的,但如果是 “多步操作”(比如
i++
,拆成 “读 i→i+1→写 i”),它管不了。
适用场景:别拿它当锁用!
Volatile轻量(不用加锁解锁,开销小),但能力有限,适合这两种场景:
-
状态标记:比如用volatile标记线程状态控制线程启停,线程 A 改状态,线程 B 立刻感知,不用频繁加锁。
-
单例模式的 “安全发布”:双重检查锁里,
volatileSingleton instance
能避免指令重排序导致的 “半初始化对象” 问题。
关键对比:别再混淆这俩!
对照表格,一眼分清核心区别不同
速记技巧:面试直接说这 3 点
被问 “Synchronized 和Volatile的区别”,抓这 3 个核心就行:
-
作用范围:Synchronized 是 “全能选手”(原子、可见、有序全保),Volatile是 “半能选手”(不能保证原子性);
-
用法场景:复杂操作(多步)用 Synchronized,简单状态标记用Volatile(注意:这里说的是相对volatile而言,需要更高级锁功能、性能优化或复杂同步逻辑的情况可以使用ReentrantLock);
-
开销差异:Volatile更轻量(无锁),Synchronized 有锁机制(但 JVM 优化后不笨重)。
最后划重点
-
关于线程安全问题。先想 “原子、可见、有序”;多步操作找 Synchronized,简单标记找Volatile;
-
牢记:Volatile不保证原子性,一些操作需要加锁,比如
i++;
搞定文章的这些内容,下次面试官再拿它们 “拷打”,直接把这些点抛回去 —— 既清晰又全面。