JMM(Java内存模型)详解

Java 内存模型(JMM,Java Memory Model)是 Java 语言规范的一部分,用于定义多线程环境下的变量访问规则,特别是共享变量的可见性和有序性。JMM 是保证并发编程正确性的核心机制之一,理解 JMM 对于编写正确的多线程程序至关重要。

1.基本概念

主内存(Main Memory): 所有变量都存储在主内存中。
工作内存(Working Memory): 每个线程都有自己的工作内存,工作内存中保存了主内存中变量的副本。线程对变量的所有操作都在工作内存中进行,之后再写回主内存。
可见性: 当一个线程修改了共享变量的值,其他线程是否能够立即看到这个修改。
有序性: 程序的执行顺序是否会按照代码的书写顺序执行,JMM 会对操作进行一定程度的重排序优化。

2.JMM的三大特性

2.1 可见性
可见性问题是指一个线程对共享变量的修改,另一个线程不一定能立即看到。JMM 通过以下机制来保证可见性:
volatile 关键字: 使用 volatile 修饰的变量,每次读取都是从主内存中读取,写操作也会立即写回主内存,从而保证变量的可见性。
锁机制(synchronizedLock): 当一个线程释放锁时,会将工作内存中的修改刷新到主内存。另一个线程获得锁时,会从主内存重新读取最新的变量值。
final 关键字: 变量被 final 修饰后,一旦被初始化,其他线程就能看见它的值(在对象构造过程中)。
2.2 有序性
有序性问题是指代码的执行顺序可能和书写顺序不同。JMM 会允许编译器和处理器对指令进行一定程度的重排序,以提高性能。但 JMM 提供了以下机制来控制重排序
程序次序规则(Program Order Rule): 单线程中,程序的执行顺序按代码的书写顺序执行。
volatile 关键字: 禁止对 volatile 变量的读/写操作进行指令重排序。
锁规则(Monitor Lock Rule): 解锁操作(unlock)先于后面对同一个锁的加锁操作(lock)。
传递性: 如果操作 A 先于操作 B,操作 B 先于操作 C,那么操作 A 先于操作 C。
2.3 原子性
原子性是指一个操作是不可中断的,即使在多线程环境下,一个原子操作一旦开始,就不会被其他线程干扰。JMM 对于以下操作提供了原子性保证:
基本类型的读取和赋值:intlong 之外的基本类型的读取和赋值操作是原子的。
锁机制: synchronizedLock 保证了临界区代码块的原子性。
CAS 操作: 通过 Atomic 包提供的类(如 AtomicInteger)实现的原子操作。

3.指令重排序和内存屏障

指令重排序是编译器和处理器在执行程序时,为了提高性能而进行的一种优化。编译器和处理器可以根据依赖关系、数据流分析等规则,对指令的执行顺序进行调整,以充分利用处理器的流水线和并行执行能力。

指令重排序的三种情况

1.编译器优化重排序
编译器在生成机器代码时,会按照优化规则重排代码顺序,以提高效率。
例如,如果两条语句之间没有数据依赖关系,编译器可能会调整它们的顺序。
2.指令级并行(ILP)重排序
在处理器内部,不同的指令可以并行执行。处理器可能会根据指令间的依赖关系动态调整指令的执行顺序。
例如,两个没有依赖关系的算术运算指令可以并行执行,顺序可能与代码中的顺序不同。
3.内存系统重排序
处理器在访问内存时,可能会为了提高性能而重排内存访问指令的顺序。
例如,处理器可能会执行后面的读操作,而推迟前面的写操作,这取决于内存访问的地址和依赖关系。

指令重排序的影响

在单线程环境下,指令重排序对程序的正确性一般没有影响。但在多线程环境中,指令重排序可能导致内存可见性问题,进而引发线程安全问题。
例如,以下代码段可能在指令重排序后出现问题

// 线程1
boolean flag = false;
int a = 0;

a = 1;       // 语句1
flag = true; // 语句2

// 线程2
if (flag) {
    // 语句3
    System.out.println(a);
}

在这种情况下,语句1和语句2可能会被重排序,导致线程2在检查 flag 为 true 时,a 仍然是0

什么是内存屏障?

内存屏障,也称为内存栅栏(Memory Fence),是一种用于防止指令重排序的机制。它是CPU指令或JVM指令,用于在多线程编程中保证某些操作的顺序性和内存可见性。

内存屏障的种类

LoadLoad 屏障:
确保在LoadLoad屏障之前的所有读取操作完成后,屏障之后的读取操作才会执行。
例子:Load1; LoadLoad; Load2,在 Load2 之前,Load1 必须完成。
StoreStore 屏障:
确保在StoreStore屏障之前的所有写入操作完成后,屏障之后的写入操作才会执行。
例子:Store1; StoreStore; Store2,在 Store2 之前,Store1 必须完成。
LoadStore 屏障:
确保在LoadStore屏障之前的所有读取操作完成后,屏障之后的写入操作才会执行。
例子:Load1; LoadStore; Store2,在 Store2 之前,Load1 必须完成。
StoreLoad 屏障:
确保在StoreLoad屏障之前的所有写入操作完成后,屏障之后的读取操作才会执行。这是最强的屏障,通常会导致CPU流水线被清空,开销较大。
例子:Store1; StoreLoad; Load2,在 Load2 之前,Store1 必须完成。

内存屏障的应用

内存屏障用于在多线程环境中控制内存操作的顺序,以确保内存可见性和防止重排序,例如:
volatile关键字
在Java中,volatile关键字用于修饰变量,确保变量的读写操作对所有线程可见。
volatile变量的写操作会插入一个 StoreLoad 屏障,确保之前的所有写操作都对其他线程可见,并阻止之后的读操作重排序到写操作之前。
锁(Locks)
锁机制(如 synchronized)会在加锁和解锁时插入内存屏障,以确保进入临界区的线程可以看到之前对共享变量的所有更新。

JMM中指令重排序与内存屏障的作用

指令重排序: 在Java内存模型中,为了优化程序的执行效率,编译器和处理器可能对指令进行重排序。但是这种优化在多线程环境下可能会导致线程安全问题,如不可见性和乱序执行。
内存屏障: 内存屏障通过阻止某些指令重排序来解决上述问题,确保多线程程序中指令的顺序性和内存操作的可见性。Java提供的 volatilesynchronized 等机制在底层都会插入适当的内存屏障来保证线程安全。

4.JMM中的Happens-Before规则

Happens-Before 规则是 JMM 的核心原则之一,用于定义操作之间的顺序关系:
程序顺序规则: 在一个线程内,代码的执行顺序按照程序代码的书写顺序。
监视器锁规则: 一个 unlock 操作在后续对同一锁的 lock 操作之前。
volatile 变量规则: 对一个 volatile 变量的写操作先行发生于后面对该 volatile 变量的读操作。
线程启动规则: Thread.start() 方法先行发生于此线程的每一个动作。
线程中断规则: 对线程的 interrupt() 调用先行发生于被中断线程的代码检测到中断事件。
线程终止规则: 线程的所有操作先行发生于此线程的终止检测(Thread.join() 返回,Thread.isAlive() 返回 false)。
对象终结规则: 对象的构造函数执行结束先行发生于 finalize() 方法的开始。

5.JMM在实际编程中的应用

理解 JMM 对于编写高效、安全的并发程序非常重要。以下是一些实际编程中的应用场景:
volatile 的正确使用: 当某个变量在多线程环境下被频繁读取且不需要复杂的同步操作时,可以使用 volatile 关键字。
synchronized 和 Lock 的选择: 在需要对共享资源进行原子操作且需要控制访问顺序时,使用 synchronizedLock
使用并发工具类: 尽量使用 java.util.concurrent 包提供的并发工具类(如 ConcurrentHashMapBlockingQueue)来避免手动处理线程安全问题。

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值