JMM内存模型介绍
Java 内存模型(Java Memory Model,JMM)是 Java 规范的一部分,它定义了 Java 程序在多线程环境下的行为,特别是关于线程之间共享变量的访问和修改。JMM 的目的是确保 Java 程序在多线程环境中具有一致性、可见性和有序性。
1. JMM 的核心概念
JMM 定义了如何处理线程间共享变量的访问,包括以下几个重要概念:
- 主内存:共享变量存储的位置,所有线程都能访问。
- 工作内存:每个线程的私有内存,存储该线程的局部变量、寄存器等数据。线程操作共享变量时,会先从主内存加载数据到工作内存中进行操作。
- 同步操作:通过
synchronized
、volatile
、final
等关键词,保证线程间的操作顺序和可见性。 - 共享变量:在多个线程间共享的数据。
2. JMM 的基本原则
- 原子性:保证基本操作(如赋值、加法、减法等)在多线程环境下是不可分割的,不会被线程调度中断。
- 可见性:一个线程对共享变量的修改,能够及时反映到其他线程中。
- 有序性:程序中的操作应该按照代码顺序执行,但由于编译优化和CPU优化,实际执行顺序可能会有所调整。JMM 保证通过适当的同步机制,线程之间的操作顺序是可控的。
3. JMM 的实现机制
JMM 通过一些底层机制来实现线程之间的同步、共享和顺序:
- 缓存一致性协议:如 MESI(Modified, Exclusive, Shared, Invalid)协议,确保每个线程的工作内存与主内存保持一致。
- 内存屏障(Memory Barrier):JMM 会插入一些内存屏障,确保指令执行的顺序符合预期。
- happens-before 关系:JMM 定义了哪些操作必须先发生,哪些操作可以后发生,以保证程序在多线程环境中的正确性。
4. JMM 中的关键字和机制
4.1 volatile
关键字
volatile
关键字用于保证变量的 可见性 和 禁止指令重排,即确保线程对变量的修改可以被其他线程立即看到。对 volatile
变量的读写操作是 原子 的,但不保证复合操作的原子性(例如 ++
操作不是原子的)。
- 可见性:当一个线程修改了
volatile
变量的值,其他线程能立即看到这个变化。 - 禁止指令重排:禁止
volatile
变量前后的代码进行重排,保证指令的执行顺序。
示例代码:
class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true; // 修改为 volatile
}
public void checkFlag() {
while (!flag) {
// 线程 1 会不停等待 flag 被更新
}
System.out.println("Flag has been updated to true");
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
Thread t1 = new Thread(example::checkFlag);
Thread t2 = new Thread(example::setFlag);
t1.start();
Thread.sleep(1000); // 确保 t1 线程先执行
t2.start();
}
}
解释:
- 线程 t1 会持续检查
flag
是否为true
,而线程 t2 会将flag
设置为true
。 - 由于
flag
是volatile
变量,线程 t1 会看到线程 t2 修改的flag
值,避免了不可见性问题。
4.2 synchronized
关键字
synchronized
关键字用于保证 原子性、可见性 和 有序性,常用来实现临界区保护,确保多个线程在访问共享资源时不发生冲突。
- 原子性:保证在同一时刻只有一个线程能够进入
synchronized
代码块。 - 可见性:进入
synchronized
代码块前,线程会将工作内存中的修改刷新到主内存;退出时,会将主内存中的数据更新到工作内存。 - 有序性:保证线程的执行顺序,避免指令重排。
示例代码:
class SynchronizedExample {
private int counter = 0;
public synchronized void increment() {
counter++; // 线程安全的加法操作
}
public synchronized int getCounter() {
return counter;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample example = new SynchronizedExample();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Counter: " + example.getCounter());
}
}
- 使用
synchronized
关键字保证counter
变量的修改是线程安全的,避免了并发问题。 - 通过对
increment()
方法加锁,确保同一时刻只有一个线程能够进入该方法,保证了原子性。
4.3 final
关键字
final
关键字在 JMM 中有特别的作用,主要用于 保证可见性 和 禁止指令重排,特别是对于对象的引用和基本数据类型的初始化。
- 对于 基本数据类型,
final
保证了变量的值在构造完成后不被改变,并且会立即对其他线程可见。 - 对于 引用类型,
final
保证了引用指向的对象不变,但对象内部的内容可以变化。
5. JMM 的 happens-before
关系
JMM 中的 happens-before
关系规定了操作之间的先后顺序,确保线程间的同步行为。主要有以下几种:
- 程序顺序规则:同一线程中的每个操作都
happens-before
后续的操作。 - 监视器锁规则:一个线程解锁一个对象的锁,另一个线程在获取该锁之前,必须看到该线程对共享变量的修改。
- volatile 变量规则:一个线程写入
volatile
变量,另一个线程读取该volatile
变量时,必须看到写入线程的最新值。 - 线程启动规则:调用
Thread.start()
之后,线程的run()
方法中的所有操作都会happens-before
线程结束之前的操作。 - 线程终止规则:一个线程的
Thread.join()
结束,必须在该线程执行完毕后执行。
6. JMM 示例:线程安全的计数器
假设我们有一个多线程环境中的计数器,我们希望确保对该计数器的修改是线程安全的,可以使用 volatile
和 synchronized
来解决。
class ThreadSafeCounter {
private volatile int counter = 0;
public synchronized void increment() {
counter++; // 原子性
}
public int getCounter() {
return counter;
}
public static void main(String[] args) throws InterruptedException {
ThreadSafeCounter counter = new ThreadSafeCounter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Counter: " + counter.getCounter()); // 2000
}
}
#牛客创作赏金赛#Java碎碎念 文章被收录于专栏
来一杯咖啡,聊聊Java的碎碎念呀