译自: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分为桟(们)和堆。下面图就是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。
若局部变量是基本类型,那么它就只存在于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里产生一份。
两个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的关系。
这是一副硬件内存结构的简图:
现代计算机通常有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和硬件内存结构不是一回事。硬件内存结构并不区分stacks和heap。在硬件中,stack和heap主要位于main memory。而部分stack和heap的数据,也会出现在cache和register中。如下图所示:
由于objecct和变量可能出现在register、cache memory以及main memory,这就导致了一些问题。主要的两个问题是:
1- 线程对共享变量更新的可见性;
2- 线程read-check-write共享变量时可能的竞态;
这两个问题在下一段讲。
如果两个或更多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。
要解决这个问题就需要用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.
下图展示了上面所说的竞态问题:
要解决这个问题需要用synchronized块。Synchronized块可以保证只有一个线程进入关键区。Synchronized也保证在块内的所有的变量访问都是从main memory中读取的, 并且当thread退出synchronized块时,所有更新了的变量都会flush回main memory, 无论该变量是否有volatile修饰。