https://learn.lianglianglee.com/专栏/深入浅出 Java 虚拟机-完/19 大厂面试题:不要搞混 JMM 与 JVM.md

本课时我们主要分析一个大厂面试题:不要搞混 JMM 与 JVM。

在面试的时候,有一个问题经常被问到,那就是 Java 的内存模型,它已经成为了面试中的标配,是非常具有原理性的一个知识点。但是,有不少人把它和 JVM 的内存布局搞混了,以至于答非所问。这个现象在一些工作多年的程序员中非常普遍,主要是因为 JMM 与多线程有关,而且相对于底层而言,很多人平常的工作就是 CRUD,很难接触到这方面的知识。

预警:本课时假设你已经熟悉 Java 并发编程的 API,且有实际的编程经验。如果不是很了解,那么本课时和下一课时的一些内容,可能会比较晦涩。

JMM 概念

在第 02 课时,就已经了解了 JVM 的内存布局,你可以认为这是 JVM 的数据存储模型;但对于 JVM 的运行时模型,还有一个和多线程相关的,且非常容易搞混的概念——Java 的内存模型(JMM,Java Memory Model)。

我们在 Java 的内存布局课时(第02课时)中,还了解了 Java 的虚拟机栈,它和线程相关,也就是我们的字节码指令其实是靠操作栈来完成的。现在,用一小段代码,来看一下这个执行引擎的一些特点。


import java.util.stream.IntStream;

public class JMMDemo {
    int value = 0;

    void add() {
        value++;
    }

    public static void main(String[] args) throws Exception {
        final int count = 100000;
        final JMMDemo demo = new JMMDemo();
        Thread t1 = new Thread(() -> IntStream.range(0, count).forEach((i) -> demo.add()));
        Thread t2 = new Thread(() -> IntStream.range(0, count).forEach((i) -> demo.add()));

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(demo.value);

上面的代码没有任何同步块,每个线程单独运行后,都会对 value 加 10 万,但执行之后,大概率不会输出 20 万。深层次的原因,我们将使用 javap 命令从字节码层面找一下。

void add();
    descriptor: ()V
    flags:
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field value:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field value:I
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LJMMDemo;

着重看一下 add 方法,可以看到一个简单的 i++ 操作,竟然有这么多的字节码,而它们都是傻乎乎按照“顺序执行”的。当它自己执行的时候不会有什么问题,但是如果放在多线程环境中,执行顺序就变得不可预料了。

Untitled