Java的内存的模型指的是Java虚拟机在计算机内存中如何工作的。Java虚拟机值整个计算机的一个模型,所以这种模型实际上包括了一个内存模型—AKA。如果你想设计一个正确的并发程序理解内存模型就显得十分重要了。Java的内存模型指的是不同的线程怎么样以什么样的方式看到共享变量,在必要的什么怎么样访问这些内存变量。原始的Java内存模型并不够,所以Java得内存模型在Java1.5之后发生变化,这种Java内存模型在Java8中依然使用。
内部的Java内存模型
Java的内存模型在Java虚拟机中内存使用,分成堆和栈,下边这个图从逻辑上解释了这个问题。
每一个线程在Java虚拟机中运行,并且有自己的线程栈。线程栈中包含了线程要调用方法的信息,我把这种称作是“调用栈”,当线程执行这个方法的时候,调用栈发生改变。线程栈同样包含了所有的方法执行时的本地变量(所有的方法都在调用栈上)一个线程值可以访问他自己的线程栈,有本线程创建的本地变量对于其他的线程值不可见的,即使两个线程执行相同的代码也是要创建属于自己的变量。因此每个线程有自己变量的版本。
所有原始类型的本地变量(boolean、byte、short、char、int、long、float和double)都是存在线程栈中的,因此对于其他的线程也是不可见的。一个线程或许可以传递原始变量的副本给其他的线程,但是它不能喝其他的线程共享自己的原始变量。堆包含在你的Java程序中创建的对象,不管线程创建的是什么样的对象都是如此的。这包含原始类型的对象版本(比如Byte、Integer和Long等等)不管对象创建还是分配给本地变量,或者作为另一个对象的成员变量都是没有关系的,对象依然是在堆上的。
下边这个图展示了栈调用和存储在线程栈上的本地变量以及堆上的对象。
一个本地变量或许是原始类型的,这种情况下它是保存在线程栈中的。
一个本地变量或许是对象的引用,这种情况引用存在于线程的栈中,但是对象本身存在于堆中。
一个对象或许包含方法方法中包含一定的本地变量。这些变量 也存在于线程栈中。对于原始类型变量和对象的引用都是成立的。
静态类变量和类的定义一道存在堆中
堆中的对象可以被所有的有这个对象引用的线程所访问。当一个线程访问一个对象的时候,它也可以访问对象的成员变量,如果两个线程同时调用同一个对象中的方法,那么他们讲会访问对象的成员变量,但是每个线程会有它自己的本地变量的拷贝。
下边这个图解释了这个问题
两个线程有本地变量的集合。一个本地变量指向一个在堆上的共享对象。这两个线程每一个都有指向同一个对象的引用。他们的引用是本地变量,并且是存储在线程的栈空间中的(每一个如此)。两个不同的引用指向同一个对象。注意共享对象(object3)有object2和object4的引用作为一个成员变量。通过object3中这个变量的引用,这两个线程可以访问object2和object4。这个图同样展示了一个本地变量同时指向了堆中的不同的两个对象。在这种情况下,引用指向两个不同的对象(object1和object5)而不是同一个对象。理论上,如果两个线程有两个对象的引用,两个线程都可以访问对象1和对象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;
}
如果两个线程执行run()方法,那么先前展示的图就是这样的结果,run方法调用methodone,methodone调用methodtwo。methodone声明了一个本地变量(int类型的localVariable1)和一个本地变量(一个对象引用)。每一个执行methodone的线程都会在栈上创建一个localVariable1和localVariable2的一个拷贝。但是两个不同的拷贝localVariable2指向堆上的同一个对象。代码把localVariable2设置为一个静态对象的引用,只有一个静态变量的拷贝并且这个拷贝是在堆上的,因此,localVariable2的两个拷贝指向同一个MyshareObject的实例,这个MyshareObject实例同样也是在堆上存储的,上图中对应于Object3。注意MyshareObject类同样也包含两个成员变量,成员变量自己在堆上存储对象,这两个成员变量指向另两个Integer对象,在上图中分别对应于对象2和对象4。
注意methodtwo创建了一个叫localvariable1的本地变量。这个本地变量是Integer对象那个的一个引用。这个方法设置了localvariable1一个新的Inter实例。localvariable1引用将会在每个线程中存储。这两个Integer对象的实例化将会存储在堆中,但是因为每次这个方法执行的时候都会创建一个新的Integer对象,两个线程将会创建不同的Integer实例,这个Integer对象在methodtwo中创建,分别对应于上图中的对象1和对象5
注意在MyshareObject中的两个变量是long类型的,因为这些变量是成员变量,他们会和对象一起存储在堆中,只有本地变量会存储在线程栈中。
硬件内存结构
现代的硬件结构和Java的内存模型是有些不同的。理解硬件内存结构对于了解Java的内存模型是如何在其中工作的是十分重要的。这一片描述了一个常见的硬件内存结构,下一个部分将会介绍Java的内存模型是如何在硬件内存模型中工作的。
下边这是一个现代计算机硬件体系的一个简单的图。
现代的计算机常常提供2个或者以上的CPU,这些CPU也可能是多核的。重点是有两个或者多个核的计算机有可能同时运行多个线程。每一个CPU在任何时候都可以运行一个线程,也就意味着如果你的Java程序是多线程的,一个线程一个CPU或许并发的在你的Java应用程序中运行。每一个CPU包含了一系列的注册器,这些注册器在CPU内存中是必须要有的。CPU在这些注册器上要比在主存上运行的更快,这是因为CPU访问注册器要比主存更快。每一个CPU有一个CPU缓存层,实际上,大多数的现代CPU都有一定大小的内存层,但通常没有直接访问内置寄存器快。所以CPU的缓存层的速度是介于内置寄存器和主存之间的。一些CPU或许有多个内存层(层一和层二),但是立即内存和Java的内存模型并没有那么重要,重要的是要知道CPU可以有不同层次级别的缓存。
一个计算接包含一个主存(RAM),所有的CPU都可以访问这个主存,主存的大小要比缓存的大小要大。通常讲当一个CPU需要访问主存的时候,它需要把主存的那一部分读到CPU的缓存中去。它或许需要把缓存的其中的一部分读到内置寄存器中去,然后在上边进行操作。当CPU需要把结果读回写回到主存的时候,它会把数据从寄存器中刷新到缓存内存中,然后在某一个时间点将数据刷新到主存中去。
当CPU需要在缓存内存中存储一些新的数据的时候,存在缓存中的值会刷新到主存中去。CPU缓存一次写一部分,也可以一次刷新一部分,当更新的时候不必全部读或者写整个内存,通常讲在小的缓存块中更新。
Java的内存模型和硬件内存模型间的关系
我们已经提到,Java的内存模型和硬件内存结构是不一样的,硬件内存结构不分线程的堆和栈,在硬件上,线程栈和线程堆都是在主存上的。线程的堆和栈有时也会在CPU的缓存中,有时也会在寄存器中,下边这个图说明了这个问题。
当对象和变量存储在计算机的不同的地方的时候,可能会发生一些问题,主要有以下两个问题:
1、线程在共享变量上的更新(写)的可见性
2、读、检查和写共享变量时的竞争条件
上边这些问题我们下边会解释
共享对象的可见性
如果两个或者两个以上的线程共享对象,而且没有正确的使用volatile或者synchronize关键字的时候,一个线程的更新堆其他的线程或许就是不可见的。想象一下那个共享的对象最初是在主存中,一个在CPU中的线程把它读到CPU的缓存中去,这会导致共享对象的改变,只要这个对象还没有被刷新回到主存中去,共享对象的改变对在CPU上运行的其他线程就是不可见的。这种方式每一个线程都会有自己共享对象的拷贝,每一个拷贝在CPU缓存中不同的部分。下边这个图就展示了这种情况,一个线程在左边的CPU上运行,把共享对象复制到CPU的缓存中去,并且把它的count变量改变成2,这个改变对于在右边CPU上的其他线程是不可见的,因为这个改变还没有刷新到主存中去
为了解决这种情况,你可使用Java的volatile关键字,这个volatile关键字可以确保给定的变量从主存中正确的读数。并且在更新的时候把数据写回到主存中去。
竞争条件
如果两个或者多个对象共享一个对象,一个或多个线程更新变量的时候竞争条件就会发生。想象一下,线程A把共享对象的变量count读入到CPU的缓存中去;再想象一下线程B也是这么做,但是是在不同的CPU的缓存中,现在线程A给count增加1,线程B也是如此,现在变量已经被增加了两次,在每一个缓存中增加了一次。如果这个增加是顺序的执行,变量count会增加两次,并且把+2之后的结果写回到主存中去。但是两次增加是并发的执行而没有很好地同步。不管线程A和B把count的结果写回到内存中去,更新之后的值只是增加了1,而不是2.下边的这个图解释了这个问题。
为了解决这个问题,你可以使用Java的synchronize关键字,synchronize块保证了在任何时候只有一个线程可以进入关键去进行读写操作,synchronize关键字同样也保证了从主存中读取所有的变量,当线程存在于synchronize块的时候,所有更新的变量都会刷新到主存中去,而不管这些变量是不是volatile的。
这是一篇非常好的文章,翻译的不好,大家如果读的不顺畅,可以直接看下边的原文。
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html