
JMM与线程可见性——小厂笔试题复盘为什么写这篇来日方长。线程可见性题目publicclassWorker{privatebooleanreadyfalse;publicvoidwaitUntilReady(){intcount0;while(!ready){count;}System.out.println(Worker is ready);}publicvoidmakeReady(){readytrue;System.out.println(Make ready);}}线程A调用waitUntilReady()线程B稍后调用makeReady()。请回答线程A是否一定能够退出循环如果不一定原因是什么把字段改成private volatile boolean ready后能否解决本题中的可见性问题假设类中还有普通字段private int result。线程B先执行result 42再执行ready true线程A观察到ready true并退出循环后是否能够看到result的新值42请说明原因作答该题考查的是对java线程可见性的理解。**可见性是指线程之间的可见性一个线程修改的状态对另一个线程是可见的。**也就是一个线程修改的结果。另一个线程能看到。要理解java的线程可见性我们要从**Java 内存模型简称JMM**开始为更好理解前置知识此处给出第一小问的答案不一定。前置知识Java内存模型JMMJava 内存模型(简称 JMM)定义了线程和主内存之间的抽象关系即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。说白了JMM就是一份规范说明书它只负责定义规则而不涉及实现。你可以把JMM看作是 Java 定义的并发编程相关的一组规范。除了抽象了线程和主内存之间的关系之外其还规定了从 Java 源代码到 CPU 可执行指令的转化过程要遵守哪些并发相关的原则和规范。其主要目的是为了简化多线程编程增强程序的可移植性。此处直接引用JavaGuide一张图**Java 内存模型JMM**的抽象示意图如下什么是主内存什么是本地内存主内存和本地内存都是一个抽象概念都不等同于真实的物理内存主内存JMM 用它抽象线程之间可以共享的变量包括实例字段、静态字段和数组元素。方法局部变量、形参和异常处理参数不会在线程之间共享因此不属于这里所说的共享变量。主内存是规范层面的抽象不等同于某一块物理内存。本地内存每个线程都有一个私有的本地内存本地内存存储了该线程已读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量无法直接访问其他线程的本地内存。如果线程间需要通信必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念并不真实存在它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。线程 1 与线程 2 之间如果要进行通信的话必须要经历下面 2 个步骤线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。线程 2 到主内存中读取对应的共享变量的值。也就是说线程间的共享数据需要遵守 JMM 的规则进行通信通过volatile、锁、线程启动与终止等方式建立相应的 happens-before 关系时JMM 会为其相关写入提供可见性保证。Java内存区域和JMM的区别JVM 内存结构和 Java 虚拟机JVM的运行时区域相关定义了 JVM 在运行时如何分区存储程序数据就比如说堆主要用于存放对象实例。Java 内存模型JMM和 Java 的并发编程相关抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范其主要目的是为了简化多线程编程增强程序可移植性的。什么是 happens-before定义如果 A happens-before B那么 A 的执行结果对 B 是可见的并且 A 的执行顺序排在 B 之前。注意两点不代表物理上的先后而是逻辑上的因果关系。禁止某些特定类型的重排序保证“语义等价”的执行结果。happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面虽然这从程序员的角度上来说也并无大碍。更准确地来说它更想表达的意义是前一个操作的结果对于后一个操作是可见的无论这两个操作是否在同一个线程里。happens-before 有多条规则下面列出其中最常用的 5 条程序顺序规则一个线程内按照代码顺序书写在前面的操作 happens-before 于书写在后面的操作监视器锁规则对一个监视器的解锁 happens-before 于随后对同一个监视器的加锁volatile 变量规则对一个volatile变量的写操作 happens-before 于随后对同一个变量的读操作传递规则如果 A happens-before B且 B happens-before C那么 A happens-before C线程启动规则Thread 对象的start()方法 happens-before 于此线程的每一个动作。happens-before规则是 JMM 对程序员的承诺这使得程序员不需要关心底层复杂的重排序细节只需要按照这些规则编写代码就能保证多线程下的内存可见性。参考答案线程A是否一定能够退出循环如果不一定原因是什么不一定原因在于可见性问题表面原因语法层面ready是一个普通的实例变量没有使用任何同步机制如volatile,synchronized,Lock规范原因JMM规范层面线程B对ready的写入操作 与 线程A对ready的读取操作之间不存在任何跨线程的 happens-before 关系。因此JMM 不会提供可见性保证线程A可能永远读取到过期值。底层原因线程A在while(!ready)中不断读取ready的值。由于没有跨线程的同步JVM 允许将ready的值缓存到线程A的本地内存CPU寄存器或缓存中而不会每次都从主内存刷新。底层原因线程B调用makeReady()将ready设为true这个新值可能只被写入线程B的本地内存或主内存但线程A可能永远看不到这个更新导致循环无限执行。把字段改成private volatile boolean ready后能否解决本题中的可见性问题能。加了volatile后线程B对ready的写 happens-before 线程Aready的读因此B的执行结果对A是可见的volatile的特性有两个保证可见性不保证原子性。对某个volatile变量的写入 happens-before 于后续对同一变量的读取因此读线程能够看到该写入以及写入前按 happens-before 传递过来的结果。禁止指令重排。重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段可能会改变指令的执行顺序。使用volatile关键字可以禁止指令重排。可以保证volatile写之前的所有普通写操作必须在该volatile写之前完成volatile读之后的所有普通读操作必须在该volatile读之后进行假设类中还有普通字段private int result。线程B先执行result 42再执行ready true线程A观察到ready true并退出循环后是否能够看到result的新值42请说明原因能volatile 写操作之前的所有操作结果对于后续对该 volatile 变量的读操作之后的所有操作都是可见的原因首先线程B volatile ready 写与后续的线程A volatile ready读之间建立了 happens-before 关系线程B先执行result 42再执行ready true故可得到线程B中result 42happens-beforeready true由于线程A先读取volatile ready再读取result形成读取readyhappens-before 读取result根据传递性线程B中result 42happens-before 线程A中读取result。因此不仅保证了volatile变量本身的可见性还通过happens-before的传递性“顺带”保证了其前后普通变量如result的可见性。上述听起来可能有点绕我们通过代码示例来理清happens-before 关系链。publicclassWorker{privatevolatilebooleanreadyfalse;privateintresult-1;publicvoidwaitUntilReady(){intcount0;while(!ready){// 读取ready——操作3count;}System.out.println(Worker is ready);System.out.println(Result: result);// 操作4}publicvoidmakeReady(){result42;// 操作1readytrue;// 操作2System.out.println(Make ready);}}happens-before 关系链操作1 happens-before 操作2操作2 happens-before 操作3volatile 写 happens-before volatile 读操作3 happens-before 操作4happens-before 关系链操作1 happens-before 操作2操作2 happens-before 操作3volatile 写 happens-before volatile 读操作3 happens-before 操作4根据传递性操作1 happens-before 操作4因此保证了普通变量result的可见性