[翻译]JSR 133 (Java Memory Model.html) FAQ
目录[TOC]原文地址： http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#whatismm 作者： Jeremy Manson and Brian Goetz 时间： February 2004到底什么是内存模型？在一个多处理器系统中，处理器通常拥有多层缓存。这些缓存能够高速获取数据（因为缓存中的数据离处理器更近，访问路径为：CPU -> 寄存器 -> 缓存 -> 主存），并能减少主存的访问频率以降低总线的繁忙程度（因为很多操作可以通过缓存数据自满足，不需要与主存交互），从而提高 CPU 整体的运行速度。虽然缓存可以明显优化性能，但缓存技术的引入也带来了很多挑战。例如：当两个处理器同时访问同一个内存地址时会发生什么？在什么情况下这两个处理器会看到相同的值？缓存的引入是否会造成数据不一致问题？在处理器层面，内存模型需要定义必要且充足的条件限制，以保证一个处理器写入主存的数据对其他处理器的可见性，并且其他处理器的写入操作也可以对当前处理器可见。一些处理器设计展现出的是强内存模型：所有处理器读到的同一内存地址的数据都是一致的。还有些处理器设计展现出的是弱内存模型：需要一些内存栅栏（Memory Barrier）去将当前处理器缓存的数据刷入主存，或将当前处理器缓存置为无效。通过这些内存栅栏来保证处理器读写的数据一致性。这些内存栅栏通常在 lock/unlock 指令发生时被执行；另外，在语言层面这些内存栅栏通常不会被程序员直接感知。在强内存模型中，通常写程序会比较简单，因为对内存屏障的使用需求较少。然而，有时在一些非常强一致的内存模型中，使用内存栅栏也是非常必要的；通常这些内存栅栏的插入位置也是违反直觉的，令程序员摸不着头脑。最近处理器设计的趋势是鼓励弱内存模型的，因为这种缓存一致性的“弱化”在多处理器、大内存容量场景下带来了更强的扩展性。编译器的重排序使得线程间可见性问题更加复杂。例如：编译器可能为了提高效率会延后执行一个程序中的写操作，当然这种重排序只要不影响程序的整体语义就是完全允许的。那么一旦编译器将一个操作重排序，使得这个操作较后执行，那么这个被重排序的操作执行之前，其他线程是无法看到的。同样的情况也会出现在缓存中。此外，写入主存操作也可以向前重排序，提前执行。那么对于其他线程来说，会看见一个被提前执行了的写入操作。所有这些允许编译器、运行时、硬件重排序的灵活性，可以使机器代码在一个最优的顺序里执行，以获取最优性能。当然这些重排序优化，都是在当前内存模型的边界范围内。以下代码可以展示一个重排序的简单示例：class Reordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; } }当以上代码被两个线程并发执行，对 y 变量的读取会看到 y 的值为 2，因为 y=2 这行代码在 x=1 之后，所以程序员就会认为读取 y 的值发现是 2，那么 x 的值肯定是 1。然而，并不一定。writer() 中的代码也许已经被重排序后执行了。实际上可能会是这样的执行情况：线程 A 执行 writer() 方法，先执行了 y=2（被排序到首行执行），接着另一个线程 B 开始执行 reader() 方法，读取到 y 的值为 2，x 的值为 0 后，线程 A 才开始执行 x=1 的代码指令。最终线程 B 看到的就是 r1=2; r2=0。JMM（Java Memory Model）描述的是多线程代码中什么样的行为是合法的，线程间如何通过主存相互通信。JMM 描述的是一个程序中变量之间的相互影响；以及一个真实的计算机中，变量在主存中或者寄存器中的读写访问细节。并且 JMM 的正确实现可以不受各种各样的硬件限制、可以不受各种各样的编译器优化策略的限制，（跨平台）实现统一的内存模型。Java 中的 volatile、synchronized、final 关键字是为了帮助程序员在 Java 语言层面向编译器提出并发性的需求。JMM 规定了 volatile、synchronized 的行为，更重要的是确保一个被正确同步的 Java 程序可以畅通无阻的在各种各样的处理器上正确执行。这个问题总结（非原文）：缓存提升 CPU 整体效率，它的两个优势：缓存比主存的使用速度快、效率高；缓存通过部分替代主存的使用，减少总线阻塞。缓存虽然能明显提高 CPU 整体性能，但会存在缓存一致性问题。CPU 层面，JMM 通常必须定义一些约束，以保证一个 CPU 对主存的写操作，能够被其他 CPU 可见。 JMM 有强内存模型、弱内存模型，这里的强指的是在这个模型中，一个 CPU（或一个线程）写入内存的数据，对其他 CPU（或其他线程）的可见性更强。强、弱内存模型都会使用内存屏障技术保障 Cache 与主存的及时同步，但是强内存模型中使用的内存屏障比较少。弱内存模型会更易于接受、有更好的扩展性。缓存、编译器为了提高效率，会对指令重排序。JMM 描述了多线程代码中什么样的行为是合法的，线程之间如何通过主存相互通信；程序中变量之间的相互关系（happens-before），以及这些变量在主存或寄存器的存取底层细节。并且不因平台的改变而受到影响。其他语言（例如 C++）有内存模型吗？除 Java 外的其他大多数语言（C、C++）的设计都不是直接支持并发的。对于限制处理器、编译器的重排序的这种必要保护，主要是依赖于第三方线程处理类库（比如 pthreads）完成。JSR 133 是什么？1997 年之后，当时的 JMM 被发现了多个严重的缺陷。这些缺陷会产生一些令人困惑的行为，比如 final 字段可以改变自己的值。另外，这些缺陷还包括 JMM 会不知不觉地破坏编译器通用的优化能力。JMM 在当时是一个很有雄心的尝试，是计算机史上第一次出现的一个编程语言的定义中包含一个 JMM 的定义，并且这个 JMM 是一个能够为并发提供一致性语义的内存模型（跨平台的）。不幸的是，定义一个兼备一致性保证和符合直觉（即能够被理解且符合描述的）的内存模型非常困难，困难程度超出了想象。JSR 133 中修复了早期 JMM 中的缺陷，定义了一个新的 JMM。为了修复这些缺陷，新的 JMM 中修改了原有 final 和 volatile 的语义。完整的语义可以在这个链接中查看 http://www.cs.umd.edu/users/pugh/java/memoryModel，但是需要提醒的是内容较为深奥，初学者慎入。这些语义惊人且清晰地展示了一些看似简单的概念却是十分复杂的，比如 synchronization。幸运的是，你不需要理解这些正式语义的细节描述。JSR 133 的目标是创建一系列正式的语义，这些语义提供一个易于理解的框架，你可以清楚地明白 volatile, synchronized, final 是如何生效的。JSR 133 的目标包括：保留现存的安全保障（例如类型安全检查），同时加强其他的部分。例如，保证变量的值不能够凭空出现；保证每个可以被其他线程可见的变量以及值，必须是可以被其他线程合理替换的值；“正确同步”的语义应该尽可能简单易懂、尽可能符合直觉；“不正确同步”或者“未完整同步”这些术语的语义应该被明确定义出来，这样可以尽可能的减少安全性被破坏的潜在风险；程序员应该能够自信地解释多线程程序中线程间如何通过内存交互；应该尽可能地去正确设计一个高性能、跨平台的 JVM 实现；应该提供一个安全初始化的新保证（规定）。一旦一个对象正确地构建完成（正确构建完成，就是指当前对象的引用没有在构造未完成时，被其他线程看到），那么其他可以看到这个对象引用的线程，都可以看到这个对象的 final 变量值（一般 final 变量在构造器中初始化），并且不需要额外的同步动作；应该尽可能减少对现有 JDK 代码的使用影响。什么是重排序？对程序中变量的访问也许会出现实际指令执行顺序与程序员在代码中编写的顺序不一致的情况。编译器为了优化程序的执行效率，可以自由的改变指令的实际执行顺序。处理器也会在某些情况下出现重排序执行指令的现象。数据在寄存器、处理器缓存、主存中的移动顺序有时会与程序代码中实际编写顺序不一致。例如，一个线程对变量 a 执行写入操作，然后对变量 b 执行写入操作，并且 a，b 这两个变量的值没有依赖关系，那么编译器可以自由的重排序这些指令，并且缓存可以不受约束的先把 b 的值刷入主存、然后再把 a 的值刷入主存。编译器、JIT、缓存都可以重排序指令。似乎应该在编译器、Runtime 和硬件的共同协作下，展现出 as-if-serial 语义。这个 as-if-serial 语义是指：在单线程程序中，程序应该是按照程序员的代码编写顺序执行，观察不到重排序的影响。然而，重排序会在没有正确同步的多线程程序中展现。在这种未正确同步的多线程程序中，一个线程可能会看到其他线程对程序的影响，可能会看到对变量的访问顺序发生了变化，比如读操作会提前读到它随后的一个写操作的数据。大多数情况下，一个线程不会在意其他线程的工作。但是一旦需要关注多个线程的活动时（并发编程），这个时候就需要考虑“正确同步”问题了！旧内存模型的缺陷是什么？旧 JMM 有很多缺陷。旧 JMM 很难让人理解，所以人们总是不经意间违反 JMM 的要求。比如，在大多数情况下，旧 JMM 不允许重排序在 JVM 中发生。正是因为旧 JMM 的这些缺陷，促使 JSR-133 中定义了新的 JMM。有一个这样的共识，如果使用了 final 修饰字段，那么就不需要在线程间额外去做同步工作了，final 可以保证被修饰字段的可见性。但是在旧 JMM 中，这个合理的假设、合理的行为并不是按照我们的想法工作的。在旧 JMM 中，final 字段与普通字段并无区别，这意味着 final 字段与普通字段一样，在多线程中必须考虑正确的同步。总之，在旧 JMM 中，我们声明一个 final 字段，并在当前对象的构造器中设置初始值的代码中，在多线程下另外一个线程可能看到这个变量的未初始化的默认值，也可能看到这个变量在构造器中初始化的值。这就意味着，不可变对象比如 String 对象，可以出现值被改变的情况，这显然是不可接受的。旧 JMM 允许 volatile 变量的写操作可以与普通变量的读操作、写操作不受约束任意重排序。这是与大多数程序员对 volatile 变量的理解大相径庭的，因此很是让人困惑。最终，旧 JMM 中程序员总是无法正确判断未正确同步的程序会出现什么样的情况。JSR-133 的一个目标就是让人们注意这个事实。“没有正确同步”是什么意思？“没有正确同步的代码”含义因人而异。当我们在 JMM 上下文中谈到“未正确同步”的代码，我们是指这样的一些代码：线程 A 执行一个变量的写操作；线程 B 执行这个变量的读操作；这两个操作没有被同步处理，也就是说他们执行的先后顺序是不确定的。当出现了以上类似情况我们通常称在这个变量上存在“数据竞争”（Data Race）。一个存在数据竞争的程序都是没有正确同步过的程序。（Synchronization）同步具体会做什么？同步有几个方面，最为人熟知的就是“互斥访问”。同一时刻只有一个线程可以获得一个 Monitor（可以理解为对象锁），所以在一个 Monitor 上的同步块只允许获得这个 Monitor 的线程进入同步块，其他线程都无法获得这个 Monitor，当然也无法进入同步块，必须等到当前同步块中的线程退出同步块，并释放这个 Monitor 后才可尝试进入。但是同步并不仅仅只是互斥执行，同步确保了一个线程在进入同步块中（或进入同步块之前）的写操作会以一种可预期的方式对同样在这个 Monitor 上同步（也就是说多个线程在一个对象锁上存在同步）的其他线程可见。当一个线程在退出 synchronized 同步块时，同时释放对象锁（Monitor），同步机制会保证当前线程的缓存数据被刷入主内存，所以这个线程在退出同步块之前的写操作对其他线程可见。在一个线程进入 synchronized 块之前，首先要尝试获取对象锁（Monitor）。这个线程获取对象锁成功，同时也会使得当前 CPU 缓存数据失效，那么就会重新从系统主存中填充本地缓存。可以看到 Monitor 对象锁的释放和获取都会导致缓存数据刷入主存、缓存数据被重新从主存更新，那么缓存数据都会被及时更新并同步主存，很明显消除了可见性问题。从缓存的角度来讨论这个问题，似乎这些问题只会影响到多处理器的机器。然而，重排序的影响在单处理器机器中也很容易被发现。例如，编译器不可能将你的代码移动到获取锁（acquire）之前或释放锁（release）之后。当我们说获取锁和释放锁作用于缓存时，我们是在简略地描述一系列可能的效果。这个新内存模型的语义实际上展现出来的是一系列内存操作（read、write、lock、unlock）和其他多线程操作（start、join）的部分重排序限制。这种“部分的重排序”也被称为 happens-before 规则。如果说 A happens-before B，那么就保证 A 会在 B 之前执行，并且 A 操作对 B 可见。具体的 happens-before 规则如下：同一个线程中的操作，都是按照代码编写的顺序执行。一个对象锁的释放一定会发生在这个锁接下来被获取的操作之前。对一个 volatile 变量的写操作一定会发生在随后的这个 volatile 变量的读操作之前。对一个线程的 start() 方法的调用一定会发生在这个线程被启动后执行的任何动作之前。一个线程中的所有操作一定会发生在其他线程成功的从这个线程的 join 方法返回之前。倘若所有的内存操作都在 Monitor 对象锁释放之前发生，并且 Monitor 对象锁的释放都发生在对象锁的获取之前，那么这就是说所有在退出阻塞块之前（释放锁之前）的内存操作，都是对其他同样获取了这个 Monitor 锁并进入了阻塞块的线程可见。以下的代码模式尝试插入内存栅栏，但是毫无用处：synchronized (new Object(.html)) {}这个实际是一个空操作，并且你的编译器将会完全移除上边的代码。因为编译器知道不会有第二个线程在同一个对象锁上阻塞（同步）。所以，这个同步毫无用处。值得注意的是：正确的使多个线程在某些代码上同步，需要让这些线程都阻塞（同步）在一个相同的对象锁上，这样才可以正确的建立 happens-before 规则。对线程 A（假设线程 A 在 Object X 上同步）可见的操作将会对线程 B 可见（假设线程 B 在 Object Y 上同步）？当然并不是！对象锁的获取与对象锁的释放必须要匹配，才能保证正确的语义（比如，获取、释放的是同一个对象锁），否则就是“没有正确同步”（或者称为存在 data race）。final 字段在新的 JMM 中如何工作？现在有这么一个对象类，它的 final 字段在这个对象的构造器中完成初始化。假设这个对象被正确地构建（构造器正确完成执行），一旦这个对象被构造完成，不需要额外的任何同步手段，这个对象的所有 final 字段（假设这些 final 字段在构造器中初始化设值）将会对其他线程可见（也就是说其他对象会见到这些 final 字段在构造器中设置的初始值）。另外，引用这些 final 字段的对象或数组都将会看到 final 字段的最新值。什么是“对象被正确地构建”？就是被构建对象的引用在构造器执行期间不会被其他线程访问到，就是说构造器执行期间，其他线程无法拿到 this！换句话说，不要在一个对象正在被构建时，把这个对象的 this 引用暴露给其他线程；不要把 this 赋给一个 static 变量；不要把 this 注册为一个 listener 等等。总之，所有可能暴露 this 的动作都需要在构造器完成之后进行！(See Safe Construction Techniques for examples..html)class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null.html) { int i = f.x; int j = f.y; } } }上边的这段代码是一个正确使用 final 字段的示例。一个执行 reader 的线程一定能看到且只能看到 f.x 的值为 3，因为 f.x 被 final 修饰过了。但是无法保证 f.y 的值一定是 4，也可能是 0，因为它不是 final 的。public FinalFieldExample() { // bad! x = 3; y = 4; // bad construction - allowing this to escape global.obj = this; }上边这段代码是一个 this 逃逸的示例，错误的初始化 final 字段。在当前线程构造对象期间，其他线程可以访问 this（通过 global.obj），因此无法保证 x 的 final 语义（也就是说其他线程可能看到 x 值为 0）！通过正确初始化含 final 字段的对象，可以正确的看到 final 变量的值（这里指的变量是基本类型）是非常好的。但是如果这个 final 变量是一个引用类型，并且你希望这个引用指向的最新的对象（或数组）及时可见怎么办？你还是希望你的代码能够看到引用所指向的这个对象（或者数组）的最新值。如果你的字段是 final 字段，那么这是能够保证的。因此，当一个 final 指针指向一个数组，你不需要担心线程能够看到引用的最新值却看不到引用所指向的数组的最新值。重复一下，这儿的“正确的”意思是“对象构造方法结尾的最新的值”而不是“最新可用的值”。现在，在讲了如上的这段之后，如果在一个线程构造了一个不可变对象之后（对象仅包含 final 字段），你希望保证这个对象被其他线程正确的查看，你仍然需要使用同步才行。例如，没有其他的方式可以保证不可变对象的引用将被第二个线程看到。使用 final 字段的程序应该仔细的调试，这需要深入而且仔细的理解并发在你的代码中是如何被管理的。如果你使用 JNI 来改变你的 final 字段，这方面的行为是没有定义的。volatile 做了什么？volatile 字段是被用来在线程间交流（通信）状态的字段。每个 volatile 的读操作都可以看到任何其他线程对这个变量的上一次（最新的）写操作的结果；(1. 可见性.html)实际上，volatile 字段的用处是杜绝读取到缓存值或者发生关于这个字段的重排序操作。(2. 禁止重排序，happens-before.html)JMM 禁止编译器以及运行时环境将 volatile 变量分配在寄存器中（通信顺序：CPU -> register -> cache -> main memory）。volatile 字段会在被写入后，立即将缓存同步到主存中去，那么这些变量因此会立即对其他线程可见。与此类似，在一个 volatile 变量被读取之前，本地处理器缓存会被置为失效，那么会直接从主存中读数据。对于对 volatile 变量的访问，还有些其他的约束。在旧 JMM 中，对于 volatile 变量的各种访问操作不能够相互重排序。但是，volatile 变量的访问却可以与非 volatile 变量的访问重排序。(也就是说，...; volatile_a=n; volatile_b=x; ...; 可以被重排序为：volatile_a=n; nonvolatile_c=f; y=nonvolatile_d; volatile_b=x;.html)。这也就是说，volatile 变量作为线程间状态通知的作用被破坏了。在新的 JMM 中，对于 volatile 变量的各种访问操作依旧不能够相互重排序。然而与旧 JMM 不同之处在于，对于普通变量的访问操作被重排序到 volatile 变量访问操作之前的这种重排序被更严格的限制了。对 volatile 变量的写操作类似于 Monitor 对象锁的释放效果，对 volatile 变量的读操作与 Monitor 对象锁的获取有同样的效果。实际上，这些都是因为新 JMM 在 volatile 访问与非 volatile 访问的重排序问题上加入了更严格的限制。这里的示例展示了 volatile 变量如何被使用：class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true.html) { // uses x - guaranteed to see 42. } } }假设线程 A 正在执行 writer 方法，线程 B 正在执行 reader 方法。在 writer 中对 v 的写操作会使得对 x 的写操作被刷入主存中，并且 v 在 reader 中的读操作会直接从主存中访问最新的数据。那么，如果 reader 执行时 v == true 成立，那么肯定能保证的是在 v 被赋值为 true 的操作之前，x=42 操作肯定被执行了。当然在旧 JMM 中就未必如此了。如果 v 不是 volatile 变量，那么编译器就可以在 writer 方法中进行重排序，那么 reader 方法中对 x 变量的读取就可能看到是 0。实际上，volatile 的语义在 JSR 133 中被充分地增强了，几乎达到了 synchronization 的程度。对 volatile 变量的读/写等价于充当了一“半”同步的作用——可见性。值得注意的是：为了使用 volatile 的语义建立 happens-before 关系，要注意两个线程必须是获取同一个 volatile 变量。并不是当 Thread A 对一个 volatile 变量 f 执行了写操作，紧接着 Thread B 对 volatile 变量 g 执行了读操作，那么 f 就对 Thread B 可见，g 就都对 Thread A 可见。读写必须要匹配以便保证正确的语义（也就是说读写必须要在同一个 volatile 变量上才行）。新 JMM 是否修复了“双重检查锁”问题？声名狼藉的 double-check locking 模式（也是一种多线程的单例发布模式）是个取巧的设计，这个设计为了支持懒加载同时想避免使用 synchronized 造成的开销。在早期的 JVM，synchronization 是非常慢的，因此程序员都排斥它。所以会有 double-check locking 模式中避免使用 synchronized 的做法：// double-checked-locking - don't do this! private static Something instance = null; public Something getInstance() { if (instance == null.html) { synchronized (this.html) { if (instance == null.html) instance = new Something(); } } return instance; }看起来似乎非常聪明，尽可能的在公共代码上缩小同步块。但是，它是错误的、无效的。为什么？instance = new Something(); 这行代码是会被编译器或者缓存重排序的，因此会导致它最终返回一个被部分构造的 Something 对象（没有完全初始化的）。结果就是我们获取了一个未初始化成功的对象。当然这个模式还存在其他问题，并且这个模式的算法修正版本也是错误的。使用旧 JMM，是无法修复这个问题的！更多细节可参考 Double-checked locking: Clever, but broken；The"Double Checked Locking is broken"declaration。很多人以为使用 volatile 关键字将会修复这个问题。在 JVM 1.5 版本以前，volatile 关键字并无法保证修复这个问题。但是在新的 JMM 中，用 volatile 修饰 instance 变量就会修复这个问题，因为一个线程 A 初始化 Something 与另一个读取并返回这个对象引用的线程 B 之间建立的 happens-before 关系。然而使用需求持有者（Demand Holder）模式更好，它不仅线程安全而且非常容易理解。(通过调用静态工厂方法，去触发一个 static 内部类的 static 变量初始化。static 变量默认的 JVM 阻塞式初始化，并且 static 变量有且仅有一例.html)private static class LazySomethingHolder { public static Something something = new Something(); } public static Something getInstance() { return LazySomethingHolder.something; }这些跟程序员有什么关系？与你肯定有关系，并发 bug 是非常难于调试解决的。这些并发bug 通常不会再测试阶段暴露，往往在系统高压下暴露出来，并且这些 bug 也难于重新并定位。你最好花些额外的精力去确保你的程序是正确同步的；然而这些并不容易，但总是要比问题出现再去定位这个未正确同步的程序容易些。也就是说预防总是要比定位解决容易些。还有少量内容，本人认为对于 JMM 理解并不重要，所以略去。说明： 本文基于 2004 年 JSR-133 规范（对应 Java 5 时代）翻译整理。虽然 Java 版本已迭代多次，但 JMM 的核心语义（happens-before、volatile、final 语义等）至今仍保持兼容。现代开发中建议优先使用 java.util.concurrent 包提供的高级并发工具。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。引言在所有编程语言中，「内存模型」（Memory Model）都是一个核心概念。区别于微架构层面的内存模型，高级语言的内存模型通常涵盖了编译器（Compiler）与微架构（Microarchitecture）两部分。笔者曾研究过 Java、C# 和 Go 语言的内存模型，发现其核心思想大同小异，主要差异在于具体实现细节。本文将重点探讨 Java 内存模型（Java Memory Model, JMM）。Java 内存模型抽象图提到 Java 内存模型，开发者通常会对下图非常熟悉：这张图揭示了线程运行时的内存交互机制：每个线程拥有一块专用的「工作内存」（Work Memory）。Java 程序将变量同步到线程所在的工作内存后，线程操作的是工作内存中的变量副本。至于工作内存中的变量值何时同步回「主内存」（Main Memory），则是不可预期的。为了保证线程安全，Java 内存模型规定通过使用关键词 synchronized 或 volatile 来满足特定的约束：volatile：保证读写操作直接针对主内存中的变量。synchronized：保证在代码块开始时将主内存的值同步到工作内存，而在块结束时将变量同步回主内存。基于以上描述，理论上我们可以写出线程安全的 Java 程序，JDK 也在很大程度上屏蔽了底层细节。工作内存的真实含义然而，当深入探究 JVM 实现时，你会发现实际上并不存在物理意义上的「工作内存」，即内存中并不会专门分配一块空间来运行 Java 程序。那么，「工作内存」究竟指代什么？这个问题曾困扰笔者许久，因为在 JVM 实现中很难找到与主内存同步的直接代码。例如，当使用 volatile 时，在源代码层面往往只能看到类似以下的汇编指令调用：__asm__ volatile ("lock; addl $0,0(%%esp.html)" : : : "cc", "memory");这条指令在部分微架构上的主要功能是防止指令重排（Instruction Reordering），即确保该指令前后的其它指令不会越过这个界限执行[注 1]。硬件层面的内存一致性在现代 x86/x64 微架构中，读写内存的一致性主要通过 MESI 协议（Intel 使用 MESI-F，AMD 使用 MOESI）来保证[注 2]。MESI 的状态转换图如下：更详细的中文文档描述可参考：http://blog.csdn.net/zhuliting/article/details/6210921编译器与微架构的优化既然物理上不存在「工作内存」，那么 Java 内存模型中所说的概念究竟映射到哪里？笔者的理解是，「工作内存」是一个虚拟概念，其承载主体主要包含两部分：编译器微架构编译器优化编译器的目标通常是执行速度越快越好。因此，编译器会尽量减少从内存读取数据的次数。如果一个数据已被加载到寄存器中，直接使用寄存器中的值性能最高。但这可能导致线程读不到最新的值。通过在 Java 语言中为变量加上 volatile 修饰符，可以强制告诉编译器该变量必须从主内存获得，此时编译器将不会进行此类缓存优化【案例可参考参考资料 5（一个 .Net 示例）】。微架构与指令重排对于微架构而言，在 x86/x64 环境下，CPU 在执行指令时可能会进行指令重排，即编译器生成的指令顺序与 CPU 实际执行的顺序可能不一致。当使用变量作为信号标志时，这种重排可能导致严重问题。例如以下代码：x = 0; y = 0; i = 0; j = 0; // thread A y = 1; x = 1; // thread B i = x; j = y;上述代码执行后，i 和 j 的值会是多少？答案是：00、01、10、11 均有可能。若希望得到确定的结果，则需要通过 synchronized（或者 java.util.concurrent.locks）来进行线程间同步。总结综上所述，笔者对 Java 内存模型的理解如下：在编译器各种优化及多种类型的微架构平台上，Java 语言规范制定者试图创建一个虚拟的概念（即 JMM），并传递给 Java 程序员，使他们能够基于这个虚拟概念写出线程安全的程序。而编译器实现者会根据 Java 语言规范中的各种约束，在不同的平台上达成程序员所需的线程安全目标。说明与参考资料版本说明：本文基于 Java 5（JSR-133）及之后的内存模型规范撰写。现代 JVM 实现细节可能随版本迭代而变化，请以官方文档为准。文中汇编示例涉及 32 位寄存器（%%esp），在 64 位环境下可能有所不同。注释与参考文献：[注 1] 关于 lock 前缀的详细说明，可查看文档《Intel® 64 and IA-32 Architectures Software Developer's Manual, Volume 3A: System Programming Guide, Part 1》的 "CHAPTER 8 MULTIPLE-PROCESSOR MANAGEMENT" 章节。[注 2] 不同的微架构内存模型存在差异，本文主要指 x86/x64 架构。想了解其他微架构的处理方式，可参考：http://gee.cs.oswego.edu/dl/jmm/cookbook.html扩展资料：《Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3A: System Programming Guide, Part 1》http://en.wikipedia.org/wiki/MESI_protocolhttp://www.javaol.net/2010/10/java-memory-model/http://gee.cs.oswego.edu/dl/jmm/cookbook.htmlhttp://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/
在Java中，它的内存管理包括两方面：内存分配（创建Java对象的时候）和内存回收，这两方面工作都是由JVM自动完成的，降低了Java程序员的学习难度，避免了像C/C++直接操作内存的危险。但是，也正因为内存管理完全由JVM负责，所以也使Java很多程序员不再关心内存分配，导致很多程序低效，耗内存。因此就有了Java程序员到最后应该去了解JVM，才能写出更高效，充分利用有限的内存的程序。1.Java在内存中的状态首先我们先写一个代码为例子：Person.javapackage test; import java.io.Serializable; public class Person implements Serializable { static final long serialVersionUID = 1L; String name; // 姓名 Person friend; //朋友 public Person() {} public Person(String name.html) { super(); this.name = name; } }Test.javapackage test; public class Test{ public static void main(String[] args.html) { Person p1 = new Person("Kevin".html); Person p2 = new Person("Rain".html); Person p3 = new Person("Sunny".html); p1.friend = p2; p3 = p2; p2 = null; } }把上面Test.java中main方面里面的对象引用画成一个从main方法开始的对象引用图的话就是这样的（顶点是对象和引用，有向边是引用关系）：当程序运行起来之后，把它在内存中的状态看成是有向图后，可以分为三种：1）可达状态：在一个对象创建后，有一个以上的引用变量引用它。在有向图中可以从起始顶点导航到该对象，那它就处于可达状态。2）可恢复状态：如果程序中某个对象不再有任何的引用变量引用它，它将先进入可恢复状态，此时从有向图的起始顶点不能再导航到该对象。在这个状态下，系统的垃圾回收机制准备回收该对象的所占用的内存，在回收之前，系统会调用finalize()方法进行资源清理，如果资源整理后重新让一个以上引用变量引用该对象，则这个对象会再次变为可达状态；否则就会进入不可达状态。3）不可达状态：当对象的所有关联都被切断，且系统调用finalize()方法进行资源清理后依旧没有使该对象变为可达状态，则这个对象将永久性失去引用并且变成不可达状态，系统才会真正的去回收该对象所占用的资源。上述三种状态的转换图如下：2.Java对对象的4种引用1）强引用 ：创建一个对象并把这个对象直接赋给一个变量，eg ：Person person = new Person("sunny".html); 不管系统资源有么的紧张，强引用的对象都绝对不会被回收，即使他以后不会再用到。2）软引用 ：通过SoftReference类实现，eg : SoftReference p = new SoftReference(new Person("Rain".html));,内存非常紧张的时候会被回收，其他时候不会被回收，所以在使用之前要判断是否为null从而判断他是否已经被回收了。3）弱引用 ：通过WeakReference类实现，eg : WeakReference p = new WeakReference(new Person("Rain".html));不管内存是否足够，系统垃圾回收时必定会回收。4）虚引用 ：不能单独使用，主要是用于追踪对象被垃圾回收的状态。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现，eg ：package test; import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; public class Test{ public static void main(String[] args.html) { //创建一个对象 Person person = new Person("Sunny".html); //创建一个引用队列 ReferenceQueue<Person> rq = new ReferenceQueue<Person>(); //创建一个虚引用，让此虚引用引用到person对象 PhantomReference<Person> pr = new PhantomReference<Person>(person, rq.html); //切断person引用变量和对象的引用 person = null; //试图取出虚引用所引用的对象 //发现程序并不能通过虚引用访问被引用对象，所以此处输出为null System.out.println(pr.get(.html)); //强制垃圾回收 System.gc(); System.runFinalization(); //因为一旦虚引用中的对象被回收后，该虚引用就会进入引用队列中 //所以用队列中最先进入队列中引用与pr进行比较，输出true System.out.println(rq.poll(.html) == pr); } }运行结果：3.Java垃圾回收机制其实Java垃圾回收主要做的是两件事：1）内存回收 2）碎片整理3.1垃圾回收算法1）串行回收（只用一个CPU）和并行回收（多个CPU才有用）：串行回收是不管系统有多少个CPU，始终只用一个CPU来执行垃圾回收操作，而并行回收就是把整个回收工作拆分成多个部分，每个部分由一个CPU负责，从而让多个CPU并行回收。并行回收的执行效率很高，但复杂度增加，另外也有一些副作用，如内存碎片增加。2）并发执行和应用程序停止 ：应用程序停止（Stop-the-world）顾名思义，其垃圾回收方式在执行垃圾回收的同时会导致应用程序的暂停。并发执行的垃圾回收虽然不会导致应用程序的暂停，但由于并发执行垃圾需要解决和应用程序的执行冲突（应用程序可能在垃圾回收的过程修改对象），因此并发执行垃圾回收的系统开销比Stop-the-world高，而且执行时需要更多的堆内存。3）压缩和不压缩和复制 ：①支持压缩的垃圾回收器（标记-压缩 = 标记清除+压缩）会把所有的可达对象搬迁到一端，然后直接清理掉端边界以外的内存，减少了内存碎片。②不压缩的垃圾回收器（标记-清除）要遍历两次，第一次先从跟开始访问所有可达对象，并将他们标记为可达状态，第二次便利整个内存区域，对未标记可达状态的对象进行回收处理。这种回收方式不压缩，不需要额外内存，但要两次遍历，会产生碎片③复制式的垃圾回收器：将堆内存分成两个相同空间，从根（类似于前面的有向图起始顶点）开始访问每一个关联的可达对象，将空间A的全部可达对象复制到空间B，然后一次性回收空间A。对于该算法而言，因为只需访问所有的可达对象，将所有的可达对象复制走之后就直接回收整个空间，完全不用理会不可达对象，所以遍历空间的成本较小，但需要巨大的复制成本和较多的内存。 ![]([binary].html)3.2堆内存的分代回收1）分代回收的依据：①对象生存时间的长短： 大部分对象在Young期间就被回收②不同代采取不同的垃圾回收策略： 新（生存时间短）老（生存时间长）对象之间很少存在引用2) 堆内存的分代：①Young代 ：Ⅰ回收机制 ：因为对象数量少，所以采用复制回收。Ⅱ组成区域 ：由1个Eden区和2个Survivor区构成，同一时间的两个Survivor区，一个用来保存对象，另一个是空的；每次进行Young代垃圾回收的时候，就把Eden，From中的可达对象复制到To区域中，一些生存时间长的就复制到了老年代，接着清除Eden，From空间，最后原来的To空间变为From空间，原来的From空间变为To空间。Ⅲ对象来源 ：绝大多数对象先分配到Eden区，一些大的对象会直接被分配到Old代中。Ⅳ回收频率 ：因为Young代对象大部分很快进入不可达状态，因此回收频率高且回收速度快②Old代 ：Ⅰ回收机制 ：采用标记压缩算法回收。Ⅱ对象来源 ：1.对象大直接进入老年代。 2.Young代中生存时间长的可达对象Ⅲ回收频率 ：因为很少对象会死掉，所以执行频率不高，而且需要较长时间来完成。③Permanent代 ：Ⅰ用 途 ：用来装载Class，方法等信息，默认为64M，不会被回收Ⅱ对象来源 ：eg：对于像Hibernate，Spring这类喜欢AOP动态生成类的框架，往往会生成大量的动态代理类，因此需要更多的Permanent代内存。所以我们经常在调试Hibernate，Spring的时候经常遇到java.lang.OutOfMemoryError:PermGen space的错误，这就是Permanent代内存耗尽所导致的错误。Ⅲ回收频率 ：不会被回收3.3常见的垃圾回收器在此之前，我们先讲一下下面将会涉及到的并发和并行两个词的解释：1）并行：指多条垃圾收集线程并行工作，但此时用户线程仍然处于等待状态；2）并发：指用户线程与 垃圾收集线程同时执行（但不一定是并行的，可能会交替执行），用户程序继续执行，而垃圾收集程序运行于另一个CPU上。好啦，继续讲垃圾回收器：1）串行回收器（只使用一个CPU）：Young代采用串行复制算法；Old代使用串行标记压缩算法（三个阶段：标记mark—清除sweep—压缩compact），回收期间程序会产生暂停，2）并行回收器：对Young代采用的算法和串行回收器一样，只是增加了多CPU并行处理； 对Old代的处理和串行回收器完全一样，依旧是单线程。3）并行压缩回收器：对Young代处理采用与并行回收器完全一样的算法；只是对Old代采用了不同的算法，其实就是划分不同的区域，然后进行标记压缩算法：① 将Old代划分成几个固定区域；② mark阶段（多线程并行），标记可达对象；③ summary阶段（串行执行），从最左边开始检验知道找到某个达到数值（可达对象密度小）的区域时，此区域及其右边区域进行压缩回收，其左端为密集区域④ compact阶段（多线程并行），识别出需要装填的区域，多线程并行的把数据复制到这些区域中。经此过程后，Old代一端密集存在大量活动对象，另一端则存在大块空间。4）并发标识—清理回收（CMS）：对Young代处理采用与并行回收器完全一样的算法；只是对Old代采用了不同的算法，但归根待地还是标记清理算法：① 初始标识（程序暂停）：标记被直接引用的对象(一级对象.html)；② 并发标识（程序运行）：通过一级对象寻找其他可达对象；③ 再标记（程序暂停）：多线程并行的重新标记之前可能因为并发而漏掉的对象（简单的说就是防遗漏）④ 并发清理（程序运行）4.内存管理小技巧1）尽量使用直接量，eg：String javaStr = "小学徒的成长历程";2）使用StringBuilder和StringBuffer进行字符串连接等操作;3）尽早释放无用对象;4）尽量少使用静态变量;5）缓存常用的对象:可以使用开源的开源缓存实现，eg：OSCache，Ehcache;6）尽量不使用finalize()方法;7）在必要的时候可以考虑使用软引用SoftReference。
目录[TOC]原文地址： http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#whatismm Jeremy Manson and Brian Goetz, February 2004到底什么是内存模型？在一个多处理器系统中，处理器通常拥有多层缓存。这些多层缓存能够高速获取数据（因为缓存中的数据与处理器更近，cpu—>寄存器—>缓存—>主存）和减少主存的访问频率以降低总线的繁忙程度(因为很多操作可以通过缓存的数据自满足，不需要在主存交互.html)来提高cpu整体的运行速度。虽然缓存可以明显的优化性能，但是缓存技术的引入也带来了很多调整。比如：当两个处理器同时访问同一个内存地址时会发生什么？在什么情况下这两个处理器会看到相同的值？缓存的引入会造成数据不一致问题?在处理器层面，一个内存模型中需要定义必要的、充足的条件限制，这些条件保证了一个处理器写入主存的数据对其他处理器的可见性，并且其他处理器的写入操作也可以对当前处理器可见。一些处理器设计所展现出的是一个强内存模型：所有处理器读到的同一内存地址的数据都是一致的。还有些处理器设计展现出一个较弱的内存模型:需要一些内存栅栏去将当前处理器缓存的数据刷入主存，或则将当前处理器缓存置为无效。通过这些内存栅栏去保证处理器读写的数据一致性。这些内存栅栏通常在lock\unlock指令发生时被被执行；另外，在语言层面这些内存栅栏是不会被程序员感知到的。在强内存模型中，通常写程序会比较简单，因为强内存模型中对内存屏障的使用需求较少。然而，有时在一些非常强一致内存模型中，使用内存栅栏也是非常必要的；通常这些内存栅栏的插入位置也是违反直觉的，令程序员摸不着头脑。最近处理器设计的趋势是鼓励弱内存模型的，因为这种缓存一致性 的"弱化"在多处理器、大内存容量场景下带来了更强的扩展性。编译器的重排序使得线程间可见性问题更加复杂。比如：编译器可能为了提高效率会延后执行一个程序中的写操作，当然这种重排序只要不影响程序的整体语义就是完全允许的。那么一旦编译器将一个操作重排序，使得这个操作较后执行，那么这个被重排序的操作执行之前， 其他线程是无法看到的。同样的情况也会出现在缓存中。此外，写入主存操作也可以向前重排序，提前执行。那么对于其他线程来说，会看见一个被提前执行了的写入操作。所有这些允许编译器、运行时、硬件重排序的灵活性，可以使机器代码在一个最优的顺序里执行，以获取最优性能。当然这些重排序优化，都是在当前内存模型的边界范围内。以下代码可以展示一个重排序的简单示例:Class Reordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; } } 当以上代码被两个线程并发执行，对y变量的读取会看到y的值为2，因为"y=2"这行代码在“x=1”之后，所以程序员就会认为 读取y的值发现是2，那么x的值肯定是1。然而，并不一定。writer()中的代码也许已经被重排序后执行了。实际上可能会是这样的执行情况：线程A执行writer()方法，先执行了y=2(被排序到首行执行.html)，接着另一个线程B开始执行reader()方法，读取到y的值为2，x的值为1后，线程A才开始执行x=1的代码指令。最终线程B看到的就是 r12;r20。JMM描述的是多线程代码中什么样的行为是合法的，线程间如何通过主存相互通信。JMM描述的是一个程序中变量之间的相互影响；以及一个真实的计算机中，变量在主存中或者寄存器中的读写访问细节。并且JMM的正确实现可以不受各种各样的硬件限制、可以不受 各种各样的编译器优化策略的限制，(跨平台.html)，实现统一的内存模型。java中的volatile、synchronized、final关键字是为了帮助程序员在java语言层面向编译器提出并发性的需求。JMM规定了 volatile、synchronized的行为，更重要的是确保一个被正确同步的java程序可以畅通无阻的在各种各样的处理器上正确执行这个问题的总结（非原文）： 1.缓存提升cpu整体效率，它的两个优势：缓存比主存的使用速度快效率高；缓存通过部门替代主存的使用，减少总线阻塞； 2.缓存虽然能明显提高cpu整体新能，但会存在缓存一致性问题。 3.cpu层面，JMM通常必须定义一些约束，以保证一个cpu对主存的写操作，能够被其他cpu可见。JMM有强内存模型、弱内存模型，这里的强指的是在这个模型中，一个cpu(或一个线程.html)写入内存的数据，对其他cpu(或其他线程.html)的可见性更强。强、弱内存模型都会使用内存屏障技术保障cache与主存的及时同步，但是强内存模型中使用的内存屏障比较少。弱内存模型会更易于接受、有更好的扩展性 4.缓存、编译器为了提高效率，会对指令重排序。 5.JMM描述了多线程代码中什么样的行为是合法的，线程之间如何通过主存相互通信；程序中变量之间的相互关系(happen-before.html),以及这些变量在主存或寄存器的存取底层细节。并且不因平台的改变而受到影响其他语言(例如C++ .html)有内存模型吗？除java外的其他大多数语言（C、C++）的设计都不是直接支持并发的。对于限制处理器、编译器的重排序的这种必要保护，主要是依赖于第三方线程处理类库(比如 pthreads.html)完成。JSR133是什么？1997年之后，当时的JMM被发现了多个严重的缺陷。这些缺陷会产生一些令人困惑的行为，比如final 字段可以改变自己的值，另外，这些缺陷还包括JMM会不知不觉的破坏编译器通用的优化能力。JMM在当时是一个很有雄心的尝试，是计算机史上第一次出现的一个编程语言的定义中包含一个JMM的定义，并且这个JMM是一个能够为并发提供一致性语义的内存模型(跨平台的.html)。不幸的是，定义一个兼备一致性保证和符合直觉(就是能够被理解的符合描述的.html)的内存模型非常困难，困难程度超出了想象。JSR133中修复了早起JMM中的缺陷，定以了一个新的JMM。为了修复这些缺陷，新的JMM中修改了原有final和volatile的语义。完整的语义可以在这个链接中查看 http://www.cs.umd.edu/users/pugh/java/memoryModel ，但是需要提醒的是胆小者勿入。这些语义惊人的、清醒的展示了一些看似简单的概念却是十分复杂的，比如 synchronization。幸运的是，你不需要理解这些正式语义的细节描述。JSR133的目标是创建一系列正式的语义，这些语义提供一个易于理解的框架，你可以清楚的明白 volatile, synchronized,final是如何生效的。JSR133的目标包括:保留现存的安全保障（例如类型安全检查），同时加强其他的部分。例如，保证 变量的值不能够凭空出现；保证 每个可以被其他线程可见的变量以及值，必须是可以被其他线程合理替换的值；"正确同步 "的语义应该尽可能简单易懂、尽可能符合直接"不正确同步"或者“未完整同步”这些术语的语义应该被明确定义出来，这样可以尽可能的减少安全性被破坏的潜在风险。程序员应该能够自信的解释多线程程序中线程间如何通过内存交互；应该尽可能的去正确设计一个高性能、跨平台的JVM实现。应该提供一个安全初始化的新保证(规定.html)。一旦一个对象正确的构建完成(正确构建完成，就是指当前对象的引用没有在构造未完成时，被其他线程看到.html)，那么其他可以看到这个对象引用的线程，都可以看到这个对象的final 变量值(一般final变量在构造器中初始化.html)，并且不需要额外的同步动作。应该尽可能减少对现有JDK代码的使用影响。什么是重排序？对程序中变量 的访问也许会出现实际指令执行顺序与程序员在代码中编写的顺序不一致的情况。编译器为了优化程序的执行效率，可以自由的改变指令的实际执行顺序。 处理器也会在某些情况下出现重排序执行指令的现象。数据在寄存器、处理器缓存、主存中的移动顺序有时会与程序代码中实际编写顺序不一致的情况。例如，一个线程对变量 a 执行写入操作，然后对变量 b执行写入操作，并且 a ，b 这两个变量的值没有依赖关系，那么编译器可以自由的重排序这些指令，并且缓存可以不受约束的先把 b的值刷入主存、然后再把a的值刷入主存。 编译器、JIT、缓存都可以重排序指令。似乎应该在编译器、runtime和硬件的共同协作下，展现出as-if-serial语义。这个as-if-serial语义是指：在单线程程序中，程序应该是按照程序员的代码编写顺序执行，观察不到重排序的影响。 然而，重排序会在没有正确同步的多线程程序中展现。在这种为正确同步的多线程程序中，一个线程可能会看到其他线程对程序的影响，可能会看到对变量的访问顺序发生了变化，比如读操作会提前读到它随后的一个写操作的数据。大多数情况下，一个线程不会在意其他线程的工作。但是一旦需要关注多个线程的活动时(并发编程.html)，这个时候就需要考虑“正确同步”问题了！旧内存模型的缺陷是什么？旧JMM有很多缺陷。旧JMM很难让人理解，所以人们总是不经意间违反JMM的要求。比如，在大多数情况下，旧JMM不允许重排序在JVM中发生。 正是因为旧JMM的这些缺陷，促使JSR-133中定义了新的JMM.有一个这样的共识，如果使用了final修饰字段，那么就不需要在线程间额外去做同步工作了，final可以保证被修饰字段的可见性。 但是在旧JMM中，这个合理的假设、合理的行为并不是按照我们的想法工作的。在旧JMM中，final字段与普通字段并无区别，这意味着final字段与普通字段一样，在多线程中必须考虑正确的同步。 总之，在旧JMM中，我们声明一个final字段，并在当前对象的构造器中设置初始值的代码中，在多线程下另外一个线程可能看到这个变量的未初始化的默认值，也可能看到这个变量在构造器中初始化的值。 这就以为着，不可变对象比如String对象，可以出现值被改变的情况,这是多么令人担忧呀！旧JMM允许 volatile变量的写操作可以与 普通变量的读操作、写操作 不受约束任意重排序。这是与大多数程序员对volatile变量的理解大相径庭的，因此很是让人困惑。最终，旧JMM中程序员在总是无法正确判断未正确同步的程序会出现什么样的情况。 JSR-133的一个目标就是让人们注意这个事实。“没有正确同步”是什么意思？"没有正确同步的代码” 含义因人而异。当我们再JMM上下文中谈到“未正确同步”的代码，我们是指这样的一些代码：1.线程A执行一个变量的写操作； 2.线程B执行这个变量的读操作； 3.这两个操作没有被同步处理，也就是说他们执行的先后顺序是不确定的。当出现了以上类似情况我们通常称在这个变量上存在“数据竞争”。一个存在数据竞争的程序都是没有正确同步过的程序。（synchronization）同步具体会做什么？同步有几个方面,最为人熟知的就是“互斥访问"。同一时刻只有一个线程可以获得一个monitor（可以理解为对象锁）**，所以在一个monitor上的同步块只允许获得这个monitor的线程进入同步块，其他线程都无法获得这个monitor，当然也无法进入同步块，必须等到当前同步块中的线程退出同步块，并释放这个monitor后才可尝试进入。但是同步并不仅仅只是互斥执行，同步确保了一个线程在进入同步块中（或进入同步块之前）的写操作会以一种可预期的方式对同样在这个monitor上同步(也就是说多个线程在一个对象锁上存在同步.html)的其他线程可见。 当一个线程在退出synchronized同步块时，同时释放对象锁（monitor）,同步机制会保证当前线程的缓存数据被刷入主内存，所以这个线程在退出同步块之前的写操作对其他线程可见。在一个线程进入synchronized 块之前，首先要尝试获取对象锁（monitor）。这个线程获取对象锁成功，同时也会使得当前cpu缓存数据失效，那么就会重新从系统主存中填充本地缓存。可以看到monitor对象锁的释放和获取都会导致缓存数据刷入主存、缓存数据被重新从主存更新，那么缓存数据都会被即使更新并同步主存，很明显消除了可见性问题。从缓存的角度来讨论这个问题，似乎这些问题只会影响到多处理器的机器。然而，重排序的影响在单处理器机器中也很容易被发现。 It is not possible, for example, for the compiler to move your code before an acquire or after a release. When we say that acquires and releases act on caches, we are using shorthand for a number of possible effects.这个新内存模型的语义实际上展现出来的是一系列内存操作（read 、write、lock、unlock）和其他多线程操作（start、join）的部分重排序。 这种“部分的重排序”也被称为 happen-before规则。如果说A happen before B，那么就保证A会在B之前执行，并且A操作对B可见。具体的happen-before规则如下：* 同一个线程中的操作，都是按照代码编写的顺序执行。 * 一个对象锁的释放 一定会发生在 这个锁接下来被获取的操作 之前 * 对一个volatile变量的写操作 一定会发生在 随后的这个volatile变量的读操作 之前 * 对一个线程的start()方法的调用一定会发生在 这个线程被启动后执行的任何动作 之前 * 一个线程中的所有操作 一定会发生在 其他线程成功的从这个线程的join方法返回 之前 倘若所有的内存操作都在monitor对象锁释放之前发生，并且monitor对象锁的释放都发生在对象锁的获取之前，那么这就是说所有在退出阻塞块之前（释放锁之前）的内存操作，都是对其他同样获取了这个monitor锁并进入了阻塞块的线程可见。以下的代码模式尝试插入内存栅栏，但是毫无用处： synchronized (new Object(.html)) {}这个实际是一个空操作，并且你的编译器将会完全移除上边的代码。因为编译器知道不会有第二个线程在同一个对象锁上阻塞（同步）。所以，这个同步毫无用处。值得注意的是: 正确的使多个线程在某些代码上同步，需要让这线程都阻塞（同步）在一个相同的对象锁上，这样才可以正确的建立happen-before规则。 对线程a（假设线程A在object X 上同步）可见的操作将会 对线程B可见（假设线程B在objectY上同步）？然后并不是！ 对象锁的获取与对象锁的释放必须要匹配，才能保证正确的语义（比如，获取、释放 的是同一个对象锁），否则就是“没有正确同步”（或者称为存在data race）。final字段在新的JMM中如何工作？现在有这么一个对象类，它的final字段在这个对象的构造器中完成初始化。假设这个对象被正确地构建（构造器正确完成执行），一旦这个对象被构造完成，不需要额外的任何同步手段，这个对象的所有final字段（假设这些final字段在构造器中初始化设值）将会对其他线程可见（也就是说其他对象会见到这些final字段在构造器中设置的初始值）。另外，引用这些final字段的对象或数组都将会看到final字段的最新值。什么是“对象被正确地构建”？就是被构建对象的引用在构造器执行期间不会被其他线程访问到，就是说构造器执行期间，其他线程无法拿到"this"！ 换句话说，不要在一个对象正在被构建时，把这个对象的this引用暴露给其他线程；不要把this赋给一个static 变量；不要把this注册为一个listener等等。 总之，所有可能暴露this的动作都需要在构造器完成之后进行！(See Safe Construction Techniques for examples..html)class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null.html) { int i = f.x; int j = f.y; } } } 上边的这段代码是一个正确使用final字段的示例。一个执行reader的线程一定能看到且只能看到f.x的值为3，因为f.x被final修饰过了。但是无法保证f.y的值一定是4，也可能是0.因为它不是final的。public FinalFieldExample() { // bad! x = 3; y = 4; // bad construction - allowing this to escape global.obj = this; } 上边这段代码是一个this逃逸的示例，错误的初始化final字段。在当前线程构造对象期间，其他线程可以访问this（通过global.obj）,因此无法保证x的final语义（也就是说其他线程可能看到x值为0）！通过正确初始化含final字段的对象，可以正确的看到final变量的值（这里指的变量时基本类型）是非常好的。但是如果这个final变量是一个引用类型，并且你希望这个引用指向的最新的对象（或数组）及时可见怎么办？你还是希望你的代码能够看到引用所指向的这个对象（或者数组）的最新值。如果你的字段是final字段，那么这是能够保证的。因此，当一个final指针指向一个数组，你不需要担心线程能够看到引用的最新值却看不到引用所指向的数组的最新值。重复一下，这儿的“正确的”的意思是“对象构造方法结尾的最新的值”而不是“最新可用的值”。现在，在讲了如上的这段之后，如果在一个线程构造了一个不可变对象之后（对象仅包含final字段），你希望保证这个对象被其他线程正确的查看，你仍然需要使用同步才行。例如，没有其他的方式可以保证不可变对象的引用将被第二个线程看到。使用final字段的程序应该仔细的调试，这需要深入而且仔细的理解并发在你的代码中是如何被管理的。如果你使用JNI来改变你的final字段，这方面的行为是没有定义的volatile做了什么？volatile字段是被用来在线程间交流（通信）状态的字段。每个volatile的读操作都可以看到任何其他线程对这个变量的上一次（最新的）写操作的结果；(1.可见性.html)实际上，volatile字段的用处是杜绝读取到缓存值或者发生关于这个字段的重排序操作。(2.禁止重排序，happen-before.html) JMM禁止编译器以及运行时环境将volatile变量分配在寄存器中（通信顺序：cpu—>register—>cache—>main memroy）。volatile字段会在被写入后，立即将缓存同步到主存中去，那么这些变量因此会立即对其他线程可见。 与此类似，在一个volatile变量被读取之前，本地处理器缓存会被置为失效，那么会直接从主存中读数据。对于对volatile变量的访问，还有些其他的约束。在旧JMM中， 对于volatile变量的各种访问操作不能够相互重排序。但是，volatile变量的访问却可以与非volatile变量的访问重排序 (也就是说，…; volatile\_a=n;volatile\_b=x; …;可以被重排序为：volatile\_a=n;nonvolatile\_c=f;y=nonvolatile\_d; volatile\_b=x; .html)。 这也就是说，volatile变量最为线程间状态通知的作用被破坏了。在新的JMM中，对于volatile变量的各种访问操作依旧不能够相互重排序。然而与旧JMM不同之处在于，对于普通变量的访问操作被重排序到volatile变量访问操作之前的这种重排序被更严格的限制了。 对volatile变量的写操作 类似于monitor对象锁的释放效果，对volatile变量的读操作与monitor对象锁的获取有同样的效果。 实际上，这些都是因为新JMM在volatile访问 与非volatile访问的重排序问题上加入了更严格的限制。 这里的示例展示了volatile变量如何被使用:class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true.html) { //uses x - guaranteed to see 42. } } } 假设线程A正在执行writer方法，线程B正在执行reader方法。在writer中对v的写操作会使得对 x 的写操作被刷入主存中，并且 v 在reader中的读操作会直接从主存中访问最新的数据。 那么，如果reader执行时“v == true ”成立，那么肯定能保证的是在 v 被赋值为 true 的操作之前，“x=42”操作肯定被执行了。当然在旧JMM中就未必如此了。如果 v 不是volatile变量，那么编译器就可以在 writer方法中进行重排序，那么 reader方法中对x 变量的读取就可能看到是0。实际上，volatile的语义在JSR133中被充分地增强了，几乎达到了synchronization的程度。对volatile变量的读\写等价于充当了一“半”同步的作用——可见性。值得注意的是:为了使用volatile的语义建立happen-before关系，要注意两个线程必须是获取同一个volatile变量。并不是当threadA对一个volatile变量 f 执行了写操作，紧接着threadB对volatile变量 g 执行了读操作， 那么 f 就对threadB可见， g.就都对threadA可见。读写必须要匹配以便保证正确的语义（也就是说读写必须要在同一个volatile 变量上才行）。新JMM是否修复了 “双重检查锁”问题？声名狼藉的 double-check locking 模式(也是一种多线程的单利发布模式.html)是个取巧的设计，这个设计为了支持懒加载同时想避免使用synchronized造成的开销。 在 早期的JVM,synchronization 是非常慢的，因此程序员都排斥它。所以会有double-check locking 模式中避免使用synchronized的做法： // double-checked-locking - don’t do this!private static Something instance = null; public Something getInstance() { if (instance == null.html) { synchronized (this.html) { if (instance == null.html) instance = new Something(); } } return instance; } 看起来似乎非常聪明，尽可能的在公共代码上缩小同步块。但是，它是错误的、无效的。为什么？ instance = new Something();这行代码是会被编译器或者缓存重排序的，因此会导致它最终返回一个被部分构造的 Something对象（没有完全初始化的）。结果就是我们获取了一个未初始化成功的对象。当然这个模式还存在其他问题，并且这个模式 得算法修正版本也是错误的。使用旧JMM，是无法修复这个问题的！ 更多细节可参考 Double-checked locking: Clever, but broken；The “Double Checked Locking is broken” declaration。很多人以为使用volatile关键字将会修复这个问题。在JVM1.5版本以前，volatile关键字并无法保证修复这个问题。但是在新的JMM中，用volatile 修饰 instance 变量就会修复这个问题，因为一个线程A初始化 Something 与另一个读取并返回这个对象引用的线程B 之间建立的 happen-before关系。然而使用需求持有者（ Demand Holder）模式更好，它不仅线程安全而且非常容易理解。 (通过调用静态工厂方法，去触发一个static内部类的static变量初始化。static变量默认的JVM阻塞式初始化，并且static变量有且仅有一例.html)private static class LazySomethingHolder { public static Something something = new Something(); } public static Something getInstance() { return LazySomethingHolder.something; } 这些跟程序员有什么关系?与你肯定有关系，并发bug是非常难于调试解决的。 这些并发bug通常不会再测试阶段暴露，往往在系统高压下暴露出来, 并且这些bug也难于重新并定位. 你最好花些额外的精力去确保你的程序是正确同步的；然而这些并不容易，但总是要比问题出现再去定位这个未正确同步的程序容易些。也就说预防总是要比定位解决容易些。还有少量内容，本人认为对于JMM理解并不重要，所以略去。