
你有没有遇到过这样的情况:明明写了一段看起来没问题的多线程代码,可运行结果却出人意料?要是你已经被这些并发问题折磨过,那就得好好了解Java内存模型和volatile关键字了。跟我一起来看看,怎样彻底搞懂这个面试常考点!
Java内存模型(JMM)是Java虚拟机规范中定义的一组规则,它描述了Java程序中各个变量(实例字段、静态字段和数组元素)的访问方式。说白了,就是规定了一个线程对共享变量的写入何时对另一个线程可见。
JMM的存在是因为现代计算机为了提高性能,在CPU和内存之间加入了多级缓存。每个CPU都有自己的缓存,多线程下可能导致各个线程看到的同一个变量的值不一样。
// 没有正确同步的情况下,这段代码的结果可能出人意料
publicclass WithoutSync {
privateboolean flag = false;
public void writer() {
flag = true; // 线程A修改变量
}
public void reader() {
if (flag) { // 线程B读取变量
System.out.println("看到了修改!");
}
}
}
可见性指的是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。缓存机制导致可见性问题:线程A修改了变量,但线程B可能看不到,因为这个新值还停留在A的缓存中没有刷新到主内存。
原子性是指一个操作不可中断,要么全部执行成功,要么全部不执行。Java中简单的赋值操作(如int a = 1)是原子的,但像i++这样的操作实际上分为读取、计算、写入三步,不是原子的。
有序性问题是指为了提高性能,编译器和处理器常常会对指令进行重排序,在单线程下不会出问题,但在多线程环境可能导致意外结果。
// 指令重排可能导致的问题
int a = ;
boolean flag = false;
// 线程A
a = ;
flag = true;
// 线程B
if (flag) {
// 这里可能看到a=0,因为指令重排可能导致flag=true先执行
System.out.println(a);
}
volatile关键字是Java提供的一种轻量级同步机制,它能保证变量的可见性和有序性,但不保证原子性。
public class WithVolatile {
privatevolatileboolean flag = false; // 使用volatile修饰
public void writer() {
flag = true; // 线程A修改变量
}
public void reader() {
if (flag) { // 线程B保证能看到最新值
System.out.println("一定能看到修改!");
}
}
}
添加volatile后,当一个线程修改了这个变量,会立即把这个新值刷新到主内存;而其他线程读取时,会直接从主内存读取,而不是从各自的缓存。
温馨提示:volatile不能替代锁!如果操作不是原子的(比如i++),volatile无法保证线程安全。
volatile的工作原理是通过插入内存屏障(Memory Barrier)指令来保证可见性和有序性。
内存屏障阻止了指令重排序,同时强制刷新缓存。JMM为volatile变量的写操作之后插入一个写屏障,为读操作之前插入一个读屏障。
状态标志:最常见的用法是用作状态标志,表示是否完成初始化或者是否中断循环。
// 用volatile实现安全的中断线程
publicclass TaskRunner {
privatevolatileboolean stopped = false;
public void run() {
while (!stopped) {
// 执行任务...
doSomething();
}
}
public void stop() {
stopped = true; // 其他线程调用stop方法来中断任务
}
}
双重检查锁定:在单例模式中使用volatile防止半初始化状态。
public class Singleton {
privatestaticvolatile Singleton instance; // volatile很关键
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
没有volatile的话,其他线程可能看到一个"没有完全初始化"的实例,因为new Singleton()操作可能被重排序。
误区一:volatile能保证复合操作的原子性。错!volatile只保证可见性和有序性。
误区二:volatile变量的所有操作都是线程安全的。错!如果有多个线程同时写一个volatile变量,结果还是不确定的。
正确做法:如果需要复合操作的原子性,应该使用锁或者java.util.concurrent包中的原子类。
// 正确使用AtomicInteger而不是volatile int
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet(); // 原子操作
}
public int getCount() {
return count.get();
}
}
理解Java内存模型和volatile关键字对并发编程至关重要。掌握了这些概念,不仅能写出更安全的多线程代码,还能在面试中脱颖而出。记得,volatile是解决可见性和有序性问题的利器,但对于原子性问题,还是得依靠锁机制或原子类。