JAVA内存模型(JMM)

时间:2021-07-25 17:01:33

译自:http://tutorials.jenkov.com/java-concurrency/java-memory-model.html

转载请保留出处:https://blog.csdn.net/csdn19900419/article/details/79812787

JMM说明了jvm是如何工作在计算机内存(RAM)上的。Jvm模拟了一个完整的计算机,因此jvm里自然也包含着一个内存模型,即JMM。 

理解JMM对设计正确运行的并发程序非常重要。JMM规定了其他线程何时、如何看到一个线程对共享变量的更新,以及如何对共享变量的同步访问。 

JVM里的JMM

JVM里的JMM分为桟(们)和堆。下面图就是JMM的逻辑视图。

JAVA内存模型(JMM)

JVM里运行的每一条thread都有自己stack。Stack里包含了程序当前所调用的所有methods的信息。我称其为“调用栈”。随着thread的执行,调用栈时刻变化。 

stack里也存放这所有调用方法的局部变量。线程只能访问它属于它自己的stack。一个线程的局部变量对其他线程是不可见的。即便两个线程运行的是同一段代码也不行,只会在两个线程的stack里各自存放一个局部变量。 

基本类型局部变量(local variables of primitive types)全都存在stack里对其他thread不可见。一个线程可以拷一份基本类型局部变量给其他线程,但是却不能共享该变量本身。 

Heap存放程序中所有thread创建出的object,包括包装类型(Byte,Integer等)。无论该object是作为局部变量还是另一个object的成员变量。 

下图展示了thread stack中的调用栈、局部变量,以及heap中存放的object。

 JAVA内存模型(JMM)

若局部变量是基本类型,那么它就只存在于thread stack中。 

若局部变量是一个ref,引用着一个object,那么该ref保存在thread stack中,而object则存放在heap里。 

Object中的方法里也可能有局部变量,而这些局部变量也是存放在thread stack中的。 

Object的成员变量无论是基本类型还是ref类型,都是和object一起存在heap中的。 

类静态变量是和类定义存放在heap里的。 

放在heap里的Objects可以被所有threads通过指向该object的ref访问。能够访问某object的thread,也能进而访问该object的成员变量。 

如果两个thread同时调用同一个object的方法,虽然他们都能访问该object的成员变量,但对于方法内的局部变量,这两个thread都会在各自的thread stack里产生一份。 

 JAVA内存模型(JMM)

两个thread各有一些局部变量。LocalVariable 2 指向一个共享object 3。这两个thread各自持有着两个不同的ref指向同一个object 3. 这两个ref是局部变量,因此存在各自的thread stack里。 

注意object3 中有两个成员变量ref指向object 2和object 4。 通过这两个ref两个thread可以访问object2 和object 4. 

图中Local variable 1这个局部变量在不同的线程中指向两个不同的object 1、object 5. 

那么什么样的java代码可以产生这种情况呢?如下: 

public class MyRunnable implements Runnable() {

    public void run() {
        methodOne();
    }

    public void methodOne() {
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    }

    public void methodTwo() {
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    }
}
public class MySharedObject {

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member1 = 67890;
}

如果有两个thread执行run( ),那么就会产生上图的情况。 

每个thread都执行methodOne()在它自己的thread stack中产生它自己的那份localVariable1和localVariable2。两份localVariable1是完全隔离的。 

而两份localVariable2则都指向同一个object 3. 

Object 3自己有两个成员变量,这两个成员变量也是存在heap里的。这两个成员变量指向object2和object4. 

MethodTwo()也创建了一个局部变量叫localVariable1。这个局部变量是一个ref,指向一个object 1。

因为这个方法每次执行都创建一个新的object,所以两个thread会各自创建出一个object,即object1和object5. 

注意object3中两个long类型的成员变量,虽然是基本类型,但也是存在heap中的。只有局部变量是存在thread stack里的。 

硬件内存结构 

现代硬件内存结构和JMM是不同的。理解硬件内存结构以及它和JMM之间的关系,也是非常重要的。

这一段讲常用硬件内存结构,下一段将它和JMM的关系。 

这是一副硬件内存结构的简图: 

JAVA内存模型(JMM)

现代计算机通常有2个或更多CPU。有些CPU甚至有多个核心。而这样就有可能使多个thread同时运行。 

每个CPU都有一些register,CPU操作这里的变量比操作main memory里的变量要快很多。 

每个CPU也会有一个cache memory,CPU操作这里的变量也比操作main memory里的变量要快,但比操作register里的变量要慢。也就是说它们的速度是 register >cache memory > main memory.

而电脑里的main memory 是所有CPU都能访问的地方,且它要比cache memory大很多。 

通常当CPU需要访问main memory时,它会读取main memory里的一部分数据进cache。它也可能会进而将数据读进register然后对其进行操作。当CPU需要把结果写会main memory时,它会把结果从register中 flush到cache,然后在另一个时间点把数据进而flush到main memory。 

通常当CPU需要在cache中存一些东西时,它会把cache原有的数据flush回mainmemory。 

JMM和硬件内存结构的联系

正如所提到的,JMM和硬件内存结构不是一回事。硬件内存结构并不区分stacks和heap。在硬件中,stack和heap主要位于main memory。而部分stack和heap的数据,也会出现在cache和register中。如下图所示: 

JAVA内存模型(JMM)

由于objecct和变量可能出现在register、cache memory以及main memory,这就导致了一些问题。主要的两个问题是:

1-   线程对共享变量更新的可见性;

2-   线程read-check-write共享变量时可能的竞态;

这两个问题在下一段讲。 

共享object的可见性

如果两个或更多thread共享同一个object,且没用volatile声明也没用synchronization机制,那么一个thread对该object的更新可能对另一个thread会是不可见的。 

设想,该object最初保存在mainmemory,CPU1上的thread读取该object到它的cache,然后对其进行更改。只要该CPU cache下次flush回mainmemory之前,这个更改对其他thread就是不可见的。这样每个thread都可能在各自的cache中保留不同版本的object。 

如下图。所编CPU中的thread复制一份共享object到它的cache中,然后更改object.count值。这个更新对于右边CPU中的thread而言就是不可见的。因为那个更新还没有被flush回main memory。 

JAVA内存模型(JMM)

要解决这个问题就需要用volatile关键字。Volatile可以保证一个从main memory中读取的变量一旦被更改就立即能写回main memory。 

竞态

如果两个或更多thread共享同一个object,并且有超过一个thread对该object有更新操作,那么竞态就可能会发生。 

设想,如果thread A读取共享object.count到它的cache,又有thread B也这样做。然后thread A对count +1,threadB也这么做。现在该变量被加了两次,每个CPU中各一次。 

如果这两次加一操作被顺序执行,结果将是+2. 

然而,这两个+1操作却是在没有同步机制的情况下执行的。虽然执行了两次+1,但这两个thread无论哪个写会mainmemory后,都只会比原有值大1. 

下图展示了上面所说的竞态问题: 

JAVA内存模型(JMM)

要解决这个问题需要用synchronized块。Synchronized块可以保证只有一个线程进入关键区。Synchronized也保证在块内的所有的变量访问都是从main memory中读取的, 并且当thread退出synchronized块时,所有更新了的变量都会flush回main memory, 无论该变量是否有volatile修饰。