一、Java内存模型(JMM Java memory Model)

JMM 是一套规范,规定了“线程本地内存”和“主内存”之间的交互规则,而不必纠结底层实现细节。

Java内存模型定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
啥是主存?主存就是所有线程都共享的数据,例如静态变量、成员变量。
啥事工作内存?就是每个线程私有的数据,也就是对应的局部变量。

java的内存模型体现在以下几个方面:

  • 原子性:保证指令不会受到线程上下文切换的影响
  • 可见性:保证指令不会受到CPU缓存的影响
  • 有序性:保证指令不会受CPU指令并行优化的影响

可见性
首先从一个现象开始说起。下列代码中,main线程对run变量的修改对于线程T是不可见的,这也就导致线程T无法停止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class VisibilityDemo {
private static boolean running = true;

public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
// 空循环
}
System.out.println("线程终止");
});
worker.start();

Thread.sleep(1000);
running = false; // 即便设置为 false,worker 线程可能不会退出
}
}

为什么会这样呢?
因为每个线程都拥有自己的工作内存,线程从主内存中读取数据,缓存在自己的工作缓存的高速缓存中,后续每次访问都会从告诉缓存中读取。

1、初始状态,T线程刚开始读取了run的值到工作内存。
2、因为Tx线程要频繁从主存中读取run的值,JIT编译器会将run的值缓存到自己工作内存中的高速缓存,减少对主存run的访问,提高效率
3、1s之后,main线程修改了run的值,并且同步到了主存而T是从自己的工作内存中高速缓存读取这个变量的值,结果永远是旧值。

怎么解决?
volatile英雄登场,volatile(易变关键字)它可以用来修饰成员变量以及静态变量,它可以避免线程从自己的工作缓存中查找变量得到值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。(关于volatile的原理,在本文最后在说)

原子性
原子性是指一个或多个操作作为一个不可分割的整体执行,要么全部执行成功,要么全部不执行,不会出现“执行一半”的中间状态。

1. 原子性问题的本质

在Java中,单条字节码指令是原子的(如 aload, istore),但代码中的一行语句可能对应多条字节码指令。例如:

1
2
// 表面是1行代码,实际对应3条字节码指令
count++;

其实际执行步骤为:

  1. 从主存读取 count 的值到工作内存
  2. 在工作内存中执行 count + 1
  3. 将结果写回主存

若两个线程同时执行 count++,可能出现以下交错执行:
Thread1: 读取 count=0 → 计算 0+1=1
Thread2: 读取 count=0 → 计算 0+1=1
Thread1: 写入 count=1
Thread2: 写入 count=1 // 最终结果为1,而非预期的2

2. 非原子操作示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AtomicityDemo {
private static int count = 0;

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});

Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});

t1.start();
t2.start();
t1.join();
t2.join();

System.out.println("Final count: " + count); // 结果可能小于20000
}
}

输出结果不稳定(如 15342),证明 count++ 是非原子操作。

3. volatile无法解决原子性问题

即使变量被 volatile 修饰,仍会出现结果错误,因为 volatile 仅保证可见性和有序性,无法阻止线程切换导致的指令交错

4. 解决方案

4.1 synchronized 同步锁
4.2 原子类(Atomic Classes)

可见性与原子性

上面的例子体现的实际上就是可见性,它保证的是在多个线程之间,一个线程对volatile关键字修饰的变量对另外一个线程可见。但是这并不能保证原子性。
PS:synchronized语句块既可以保证原子性也可以保证代码块内变量的可见性,但是缺点就是synchronized属于重量级操作,性能较差。

原子性与可见性、有序性的关系

特性 保障手段 解决的核心问题
原子性 synchronized、原子类 线程切换导致的指令交错
可见性 volatilesynchronized CPU缓存导致的读写不一致
有序性 volatilesynchronized 编译器/CPU指令重排序

有序性

JVM会在不影响正确性的前提下,调整语句的执行顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ReorderDemo {
static /*volatile*/ int x = 0;
static /*volatile*/ int y = 0;

public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
x = 1; // 写1
y = 2; // 写2
});
Thread reader = new Thread(() -> {
int r1 = y; // 读y
int r2 = x; // 读x
if (r1 == 2 && r2 == 0) {
System.out.printf("重排序现象:y=%d, x=%d%n", r1, r2);
}
});
reader.start();
writer.start();
writer.join();
reader.join();
}
}

这样能在不加 volatile 时真实观察到“先写 y 再写 x”被重排序导致读到 (2,0) 的场景;而加上 volatile 后则绝不会出现该结果

这种特性称之为【指令重排】,在多线程下【指令重排】会影响正确性。什么是指令重排?下文就讲。

看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int num = 0;
boolean ready = false;
//线程1执行此方法
public void actor1(I_Result r){
if(ready){
r.r1=num+num;
}else{
r.r1=1
}
}
//线程2执行此方法
public void actor2(I_Result r){
num=2;
readdy=true;
}

看代码,你可能会认为:
①线程1先执行,这个时候ready=false,所以进入了else分支,结果为1.
②线程2先执行num=2,但是还没来得及执行ready=true,线程1执行,还是进入了else分支,结果为1.
③线程2先执行,执行到ready=true,线程1执行,进入if分支,相加为0,再切回线程2执行num=2

但是!执行代码可能会得到一个诡异的结果:那就是 num=0;
因为会进行指令重排,先执行了ready=true,然后执行if(ready),进入if分支,此时num=0,r.r1=0+0;
这种线程就是指令重排。

如何解决指令重排呢?依然是volatile关键字。下面介绍以下volatile关键字的原理

volatile的底层实现原理是内存屏障,Memory Barrier (Memory Fence)

  • 对volatile变量的写指令加入写屏障
  • 对volatile变量的读指令加入读屏障

1、如何保障可见性?

  • 写屏障会确保屏障之前的所有共享变量(不仅是volatile变量)的修改都同步到主存

    1
    2
    3
    4
    5
    6
    boolean volatile ready = false
    public void actor2(I_Result t){
    num =2;
    ready = ture;
    //写屏障 num=2 ready=true。同步到了主存
    }
  • 读屏障保证在该屏障之后,对共享变量的读取,加载的都是主存中的最新数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void actor1(I_Result t){
    //读屏障-----------------------------------------------------------------
    //ready是volatile修饰的,读取是带有读屏障
    if(ready){
    r.r1= = num+num;
    }else{
    r.r1=1
    }
    }

2、如何保证有序性?

  • 写屏障会确保指令重排时,不会将写屏障之前的代码排在写屏障的后面
  • 读屏障会确保指令重排时,不会讲读屏障之后的代码排在读屏障之前

再提一遍,volatile关键字并不能保证指令交错运行,即并不能保证原子性

  • 写屏障的作用主要是确保在写操作完成之前,所有对该写操作有依赖的其他写操作都对其他线程可见。并且,写屏障可以在一定程度上保证写操作的有序性,使得读操作不会被重排序到写操作的前面去。它并不直接保证后续读操作一定读到最新结果,因为这还与各种其他因素相关。
  • 有序性的保证不仅仅是为了防止本线程相关代码被重排序,更是为了确保程序的执行顺序与代码的语义顺序一致,以避免指令的重排序导致程序行为不符合预期。这种有序性保证对于单线程的正确性以及多线程中不同线程间的内存可见性和正确执行顺序都至关重要,并且它涉及到编译器、处理器等多个层面的约束和协调。

3、经典问题-double-check locking单例模式,直接上代码了

第一次检查避免不必要的同步,第二次检查防止其他线程已创建实例。volatile 防止初始化时的指令重排(如对象未完全构造就被使用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Singleton {
private Singleton() {}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

最后补一个happens-before原则

这个原则就是规定了共享变量的写操作对其它线程的读操作可见,它时可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写对于其他线程对该共享变量是可见的。

  • 1. 解锁规则(synchronization)

    规则:线程解锁前对变量的写操作,对接下来其他线程加锁后的读操作可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class SyncExample {
    private int value = 0;
    private final Object lock = new Object();

    public void writer() {
    synchronized (lock) {
    value = 42; // 写操作
    }
    }

    public void reader() {
    synchronized (lock) {
    int temp = value; // 读操作,可以读到 writer 写入的 42
    System.out.println("Read value: " + temp);
    }
    }
    }

    解释writer 方法在释放锁之前写入 value = 42reader 方法在获取锁之后可以读到这个值。

  • 2. Volatile 变量规则(volatile)

    规则:对 volatile 变量的写操作,对后续其他线程对该变量的读操作可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class VolatileExample {
    private volatile int value = 0;

    public void writer() {
    value = 42; // 写操作
    }

    public void reader() {
    int temp = value; // 读操作,可以读到 writer 写入的 42
    System.out.println("Read value: " + temp);
    }
    }

    解释value 被声明为 volatile,所以 writer 写入的值对 reader 可见。

  • 线程启动规则(Thread Start)

    规则:线程启动前对变量的写操作,对线程启动后的读操作可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ThreadStartExample {
    private int value = 0;

    public static void main(String[] args) {
    ThreadStartExample example = new ThreadStartExample();
    example.value = 42; // 写操作

    Thread thread = new Thread(() -> {
    int temp = example.value; // 读操作,可以读到 42
    System.out.println("Read value in new thread: " + temp);
    });

    thread.start(); // 启动线程
    }
    }

    解释:在启动新线程之前,主线程写入 value = 42,新线程启动后可以读到这个值。

  • 线程终止规则(Thread Termination)

    规则:线程结束前对变量的写操作,对其他线程感知到该线程结束后的读操作可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class ThreadTerminationExample {
    private int value = 0;

    public static void main(String[] args) throws InterruptedException {
    ThreadTerminationExample example = new ThreadTerminationExample();
    Thread thread = new Thread(() -> {
    example.value = 42; // 写操作
    });

    thread.start();
    thread.join(); // 等待线程结束

    int temp = example.value; // 读操作,可以读到 42
    System.out.println("Read value after thread termination: " + temp);
    }
    }

    解释:子线程写入 value = 42,主线程通过 join() 等待子线程结束,之后可以读到这个值。

  • 中断规则(Thread Interruption)

    规则:线程中断前对变量的写操作,对其他线程感知到中断后的读操作可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class InterruptionExample {
    private int value = 0;

    public static void main(String[] args) throws InterruptedException {
    InterruptionExample example = new InterruptionExample();
    Thread thread = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
    // 线程运行中
    }
    example.value = 42; // 写操作,中断后执行
    });

    thread.start();
    Thread.sleep(100); // 确保线程运行起来
    thread.interrupt(); // 中断线程

    int temp = example.value; // 读操作,可以读到 42
    System.out.println("Read value after interruption: " + temp);
    }
    }

    解释:主线程中断子线程后,子线程在中断处理中写入 value = 42,主线程可以读到这个值。

  • 默认初始化规则(Default Initialization)

    规则:变量的默认初始化值对所有线程可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class DefaultInitExample {
    private int value; // 默认值为 0

    public static void main(String[] args) {
    DefaultInitExample example = new DefaultInitExample();
    new Thread(() -> {
    int temp = example.value; // 读取默认值 0
    System.out.println("Default value: " + temp);
    }).start();
    }
    }

    解释:新线程读取 value 的默认值 0,无需任何同步。

  • 传递性规则(Transitivity)

    规则:如果操作 A happens-before B,且 B happens-before C,则 A happens-before C。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class TransitivityExample {
    private volatile int value1 = 0; // volatile 保证可见性
    private int value2 = 0;

    public void writer() {
    value1 = 42; // 写操作 1
    value2 = 84; // 写操作 2
    }

    public void reader() {
    if (value1 == 42) { // 读操作 1,根据 volatile 规则,对后续读可见
    int temp = value2; // 读操作 2,可以读到 84
    System.out.println("Read value2: " + temp);
    }
    }
    }

    解释value1 是 volatile 的,writer 写入 value1 = 42value2 = 84reader 读取 value1 == 42 后,根据传递性规则,value2 = 84reader 可见。