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)也能保证线程安全的一类算法),例如在原子类中实现无锁自增,下面是 UnsafegetAndAddInt() 方法的源码:

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

待更新...