Java内存模型JMM(Java Memory Model)

你好,我是风一样的树懒,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。

点击下方👇关注公众号,带你一起复习后端技术,看看面试考点,补充积累技术知识,每天都为面试准备积累

文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读

JMM(Java Memory Model,Java内存模型) 是 Java 并发编程中非常核心的一个概念,它定义了 Java 程序中不同线程之间如何共享内存以及如何保证内存的可见性、原子性和有序性。JMM 的设计目标是为 Java 提供一个规范,确保在多线程环境下执行时,程序的行为符合预期,而不依赖于底层硬件架构的具体实现。



01
JMM的核心目标


可见性:当一个线程修改了共享变量的值,其他线程能立刻看到这个修改。

原子性:一个操作在执行时不受其他线程的干扰,要么全部执行完,要么完全不执行。

有序性:程序代码的执行顺序应当符合代码中的顺序,即程序中按顺序写的操作应该按顺序执行。


02
JMM的核心概念


JMM 的设计背后有几个关键概念,它们解决了多线程编程中的常见问题。

主内存和工作内存

主内存(Main Memory):所有线程共享的内存区域,所有的实例字段、静态字段和数组元素都存储在主内存中。

工作内存(Working Memory):每个线程都拥有自己的工作内存,工作内存是线程对主内存的副本,线程的操作是通过工作内存来访问主内存中的数据。

当线程执行时,它会将主内存中的数据加载到工作内存中,线程通过工作内存来读取和修改数据,但修改后的数据并不会立即写回主内存,直到某个操作被强制刷新。


可见性

可见性问题是指当多个线程修改共享变量时,一个线程的修改在其他线程中不可见。JMM 提供了一些规则来保证线程之间的可见性:

线程间共享变量的更新机制:通过 volatile 关键字和 synchronized 关键字,JMM 可以确保一个线程修改的变量对于其他线程是可见的。

  • volatile:当一个变量声明为 volatile 时,JMM 保证对该变量的写操作对其他线程立即可见。同时,JMM 也保证了该变量的读取操作是从主内存中读取,而不是从工作内存中读取。

  • synchronized:synchronized 保证了互斥访问,同时,它也具有可见性,线程在进入 synchronized 方法之前,会把工作内存中的数据刷新到主内存中,线程离开 synchronized 方法时,修改的数据会被写回主内存。


原子性

原子性指的是一个操作不可分割,要么执行完成,要么完全不执行。在多线程环境下,原子性是非常重要的,JMM 保证了基本数据类型的操作是原子的,但并不保证对复合操作(如 i++)的原子性。

  • volatile 对单一变量的读写操作保证原子性,但对于复合操作(如累加、加法等),volatile 无法保证原子性。

  • synchronized 关键字可以保证一个代码块中的操作是原子的,多个线程对同一个 synchronized 代码块的访问会互斥,避免并发冲突。

有序性

有序性问题是指程序的执行顺序不一定按照代码的顺序执行。在并发环境中,编译器和处理器可能会进行指令重排序,以提高程序的执行效率。然而,这可能会导致一些问题,尤其是在多线程环境下,线程的执行顺序不能保证。

为了避免这种情况,JMM 通过 happens-before 规则 来规定操作的执行顺序,确保程序的执行顺序符合预期。

  • synchronized:进入某个 synchronized 区块之前的操作,必须发生在离开该区块之后的操作之前。也就是说,进入和离开 synchronized 区块有明确的执行顺序。

  • volatile:对 volatile 变量的写操作,发生在后续对该变量的读操作之前。也就是说,写操作和读操作之间也有执行顺序保证。


03
JMM 中的 Happens-Before 规则


为了保证有序性和正确性,JMM 定义了一系列的 happens-before 规则,确保在多线程环境下,程序的执行顺序是符合预期的。以下是几个常见的 happens-before 规则:

  • 程序顺序规则:在一个线程内,按照代码的顺序执行,前面的操作 happens-before 后面的操作。

  • 监视器锁规则:一个线程释放锁 happens-before 另一个线程获取同一个锁。

  • volatile 变量规则:对一个 volatile 变量的写操作 happens-before 后续对该变量的读操作。

  • 传递性规则:如果 A happens-before B,B happens-before C,那么 A happens-before C。


04
volatile 关键字的特殊性


volatile 关键字是 JMM 提供的用于处理可见性问题的机制。它的作用不仅仅是保证变量的可见性,还涉及到有序性。

可见性:当一个线程修改了 volatile 变量的值,其他线程可以立即看到修改的结果。

禁止指令重排序:volatile 变量的读写操作会被禁止重排序,这意味着所有对 volatile 变量的操作会按照代码中出现的顺序执行,保证了有序性。

然而,volatile 不能保证原子性。对于复合操作,仍然需要使用 synchronized 来保证线程安全。


05
JMM的实现细节


在底层实现上,JMM 主要通过以下方式来保证多线程的正确性:

  • 缓存一致性协议:如 MESI 协议(修改、独享、共享、无效)来保证多处理器系统中的缓存一致性。它确保了各个核心之间的缓存是同步的。

  • 内存屏障(Memory Barrier):内存屏障(如 StoreLoad、LoadLoad、StoreStore)是硬件级别的一种机制,它通过禁止指令重排序来保证一定的执行顺序。


06
JMM的常见问题


指令重排序:由于 JMM 的存在,编译器和处理器会为了优化性能而对指令进行重排序,可能导致意想不到的结果。例如,A线程修改了某个共享变量,而B线程读取时却看不到修改的结果,这就是由于指令重排序所造成的问题。

缓存一致性问题:由于每个线程有自己的工作内存,多个线程同时访问同一变量时可能会遇到缓存一致性问题。


07
JMM与 Java 并发包的关系


JMM 为 Java 并发编程提供了基本的内存语义,Java 的并发包(如 java.util.concurrent)则通过高层的抽象(如 Executor、CountDownLatch、Semaphore 等)帮助开发者更方便地使用并发编程模型,同时保证线程间的正确性和效率。

今天的内容就分享到这儿,喜欢的朋友可以关注,点赞。有什么不足的地方欢迎留言指出,您的关注是我前进的动力!

END


扫码关注

一起积累后端知识
不积跬步,无以至千里
不积小流,无以成江海

喜欢此内容的人还喜欢

谈谈id那些事(五)——美团的 Leaf 的ID生成


一个阿里二面面试官必问的问题


Lambda表达式说爱你不容易


分享面试:mysql数据库索引失效的情况


Spring-Boot中一个不起眼的好工具StopWatch