《深入理解 Java 内存模型》笔记总结

来源:互联网 发布:淘宝大学是几本 编辑:程序博客网 时间:2024/05/21 19:31

《深入理解 Java 内存模型》总结

  • 深入理解 Java 内存模型总结
    • 概述
    • 单线程
    • 多线程
      • volatile
      • Memory Barrier
      • final
    • 参考资料

概述

什么是内存模型呢?JSR-133 规范是这么定义的:

A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program. For the Java programming language, the memory model works by examining each read in an execution trace and checking that the write observed by that read is valid according to certain rules.
给定一个程序和该程序的一串执行轨迹,内存模型描述了该执行轨迹是否是该程序的一次合法执行。对于Java,内存模型检查执行轨迹中的每次读操作,然后根据特定规则,检验该读操作观察到的写是否合法。

简单的说,内存模型描述了某个程序的可能行为。内存模型包含一组规则,规定了一个线程的写操作何时对另一个线程可见。在程序行为满足这些规则的情况下,JVM 可以自由地进行代码转换,比如重排序和非必要的同步移除,代码转换往往是为了提升性能。

JMM(Java Memory Model) 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存,每个线程都有一个私有的本地内存,本地内存中存储了读写共享变量的副本。本地内存是一个抽象的概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
Java 内存模型

单线程

单线程程序的行为,遵守线程内语义。JSR-133 中,线程内语义的定义如下:

线程内语义是单线程程序的标准语义,基于某个线程内读动作能看到的值,可以完整的预测这个线程的行为。

通俗的讲,JVM 对代码的转换,不能改变单线程程序的执行结果,其结果要和顺序执行的结果一致。具体体现在:
1. 对同一内存对象的交替读写是有序对,不能被打乱
2. 存在数据依赖关系的两个操作的执行顺序,不能被改变

多线程

当多个线程之间存在共享数据的读写时,其行为就有了诸多的不确定性。在程序中,要保证对共享数据的正常访问,一般要使用同步原语:锁、volatile、final 等。JMM 对正确同步的多线程程序的内存一致性做了保证:

如果程序是正确同步的,程序的执行将具有顺序一致性。即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。

顺序一致性的内存模型是一个理想化了的理论参考模型,为程序员提供了极强的内存可见性保证。其有两个特性:
1. 一个线程中的所有操作按照程序的顺序来执行
2. 所有线程都只能看到一个单一的操作执行顺序,每个操作都必须原子执行而且立即对所有线程可见

也就是说,整个程序依照着一种有序的执行顺序执行。顺序一致性模型过于严格,为了提升执行效率,JMM 允许在不改变程序执行结果的前提下,做一些编译器和处理器优化。

下面总结一下常见的同步原语。

volatile

当我们声明一个变量是 volatile 时,保证:

可见性:对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
原子性:对任意单个 volatile 变量的读写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。

volatile 是如何实现的呢?编译器在生成字节码时,会在指令序列中,插入内存屏障 Memory Barrier 来禁止特定类型的处理器重排序。

Memory Barrier

Memory Barrier 即内存屏障,也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。

通常,各种加锁原语、关中断函数、调度函数、睡眠与唤醒函数都隐式地包含了内存屏障的功能。

锁是 Java 并发编程中最重要的同步机制。

当释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中。
当获取锁时,JMM 会把该线程对应的本地内存置为无效。

也就是说,锁的释放-获取的内存语义和 volatile 写-读的内存语义一致。

锁的具体实现,可以看 Java 中的 ReentrandLock。

final

对于 final 域,编译器和处理器要遵守两个重排序规则:

在构造函数内对一个 final 域的写入,与随后把这个构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
初次读一个包含 final 域对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序

如果 final 域是引用类型,需要满足:

在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数之外,把构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

final 的语义保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(lock 或 volatile),就可以保证任意线程都能看到这个 final 域在构造函数中被初始化之后的值。

参考资料

深入理解 Java 内存模型 程晓明
内存屏障
Linux 内核中的内存屏障

原创粉丝点击