Java 内存模型
Java 的内存模型(Java Memory Model, JMM)规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存保存了被该线程使用的变量的主内存副本。线程对变量的读取、赋值等操作都必须在工作内存中执行,而不能直接读写主内存中的数据。
比如下面的例子,有概率 永远无法退出循环(即使主线程已经写了 false
),因为另一个线程可能一直在用工作内存中的 running = true
。
static boolean running = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (running) {
// 空转
}
System.out.println("stopped");
}).start();
Thread.sleep(1000);
running = false;
}
这就是可见性的问题,一个线程对共享变量的修改,其他线程并不是立马可见的。
volatile 关键字
关键字 volatile 可以说是 Java 虚拟机提供的最轻量级的同步机制,当一个变量被定义成 volatile 之后,它将具备两项特性。
第一项是保证该变量的可见性,当一个线程修改了这个变量的值,其他线程可以立即得知。这是因为当对一个 volatile 变量赋值时,会立刻写回主内存;读取一个 volatile 变量时,会从主内存重新读取。这样可以在上面的状态标志场景中起到关键性的作用,即:修改标志变量后,其他线程可以立马得知。
public class VolatileMain {
// volatile 关键字修饰
volatile static boolean running = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
// 每次读取都从主内存刷新,可以正确停止
while (running) {
// 空转
}
System.out.println("stopped");
}).start();
Thread.sleep(1000);
running = false;
}
}
第二项是防止指令重排序。Java 源代码会经历编译器优化重排和指令并行重排等过程,在保证串行语义一致的情况下进行优化。被 volatile 修饰的变量禁止了指令重排序,禁止它前面的普通写操作被重排序到后面。
一个常见的使用场景就是双重锁校验的单例模型:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 可能重排序发生在这里!
}
}
}
return instance;
}
}
一个 new 对象赋值语句,在 JVM 层面实际上是三个步骤:
1. memory = allocate(); // 分配对象内存
2. ctor(memory); // 初始化(调用构造方法)
3. instance = memory; // 设置 instance 指向内存地址
如果没有 volatile 的约束,JVM 或 CPU 可以对这三步进行重排序,比如:
1. memory = allocate();
2. instance = memory; // 提前设置引用(对象未初始化完成!)
3. ctor(memory); // 后初始化
假设线程 A 正在执行 new Singleton()
,还没执行完构造函数(第 3 步),此时 instance 已经被设置为非 null。线程 B 执行 getInstance()
,发现 instance != null
,直接返回,获得的是一个“未初始化完成”的对象,使用它会出错(内部空指针异常)。
而使用了 volatile 关键字后,禁止它前面的所有写操作重排序到后面,可以保证构造函数中的成员赋值都会在 instance
被赋值前完成。
CAS
CAS,全称 Compare-And-Swap,中文通常翻译为比较并交换,是一种常用的原子操作机制,也是乐观锁的一种典型实现方式。在并发编程中,CAS 能够在不加锁的情况下实现线程安全,广泛应用于 Java 的原子类和同步框架中。
在 Java 中,CAS 是通过 Unsafe
类中的 native 方法实现的,这些方法底层依赖于 CPU 提供的原子指令(如 x86 的 CMPXCHG
):
/**
* 以原子方式更新对象字段的值。
* @return 如果值被成功更新,则返回 true;否则返回 false
*/
public final native boolean compareAndSwapObject(Object o, long offset,
Object expected,
Object x);
/**
* 以原子方式更新 int 类型的对象字段的值。
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
/**
* 以原子方式更新 long 类型的对象字段的值。
*/
public final native boolean compareAndSwapLong(Object o, long offset,
long expected,
long x);
CAS 操作会比较一个变量的当前值是否等于期望值(expected):
如果相等,则将其更新为新值;
如果不等,则什么都不做,返回
false
。
这个机制可以实现非阻塞算法(是指在多线程并发环境下,不依赖加锁机制(如 synchronized 或 ReentrantLock)也能保证线程安全的一类算法),例如在原子类中实现无锁自增,下面是 Unsafe
中 getAndAddInt()
方法的源码:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
该方法通过 do-while 自旋重试机制保证并发场景下的数值累加操作是安全的。如果某一时刻有其他线程修改了目标值,当前线程的 CAS 操作就会失败并进入下一轮重试,这种方式即为自旋锁的一种实现。
虽然 CAS 是无锁的高性能解决方案,但在高并发场景下,频繁的失败重试可能带来 CPU 开销。此外,它还存在 ABA 问题,即变量值从 A 变为 B 又变回 A,CAS 不会察觉。Java 提供了 AtomicStampedReference 和 AtomicMarkableReference 等类来解决这个问题。
AQS
待更新...