摘要
理解java内存模型对于设计多线程的java程序有很大的帮助
java内存模型特别的指java虚拟机如何使用计算机的内存(ram)。java虚拟机是一个计算机的模型,所以这个模型也自然的包含一个内存模型,也就是java内存模型。
对于想正确的设计并发的程序的开发者来说,理解java每次模型是非常重要的。java内存模型指出了如何处理不同线程见值写入的可见性和变量共享,以及必要时怎么同步访问共享变量。
原先的java内存模型是有缺陷的,所以java内存模型在java 1.5版本时重新设计了,而这个新的模型一直延续到如今的java8.
内置的java内存模型
jvm内置内存模型将内存区域划分为线程栈和堆两个区域。下面的图显示java内存模型的逻辑:


每个运行中jvm中的线程有它自己的线程栈,这个线程栈包含有目前它调用的方法的信息,可以把它叫做“Call Stack",当线程执行它的代码,这个"call stack”会改变。
线程栈还包含有每个准备执行的本地变量。一个线程只能访问他们自己的线程栈。一个线程创建的本地变量对其它线程是不可见的。 即便两个线程执行同一段代码,两个线程也会在各自的线程栈中创建本地变量,因此每个线程都有每个本地变量属于自己的版本。
所有的基础类型的本地变量都储存在线程栈中,而且对其他线程都是不可见的。一个线程可以拷贝一个变量到另一个线程,但无法共享。
堆中包含有一个应用中所有的对象(即非基础类型的),不管哪个线程创建的,这个也包括基础类型的包装类对象(即Long,Integer等等)。无论一个object是被创建的还是指向一个本地变量,亦或者从其他的object创建的成员变量,它都会被存储在堆中。
如下图表示:
一个对象的成员变量都存储在堆中与该对象本身一起,无论它是基础变量类型还是指向一个object。
静态的类变量也存储在堆中。
堆中的对象可以被所有的线程访问,它们可以持有对这个对象的引用,当一个线程访问一个object时,它也可以访问这个object的成员变量。如果两个线程同时调用同一对象的同一方法,它们都可以访问到这个object的成员变量,但每个线程拥有自己的拷贝作为本地变量。
如下图:
两个线程都执行同一段代码,这代码有两个方法,第一个方法有两个本地变量,其中变量2都指向一个堆对象object2,而object2又有两个成员变量object3和object4,两个线程中的方法2都有一个本地变量,该变量一个执行object1,一个指向Object6.
因此线程1和线程2可以共同访问object2,虽然他们都各自有method1中的Local variable2,但它们都指向object2,而object2又有object3和object4这两个成员变量,因此他们也有着对这两个对象的访问权。
硬件内存模型:
如下是一个对现在内存模型简单的图示:
现代计算机通常拥有两个或更多的cpu,而有的cpu还有多个核心。这意味着如果java应用程序是多线程的,那么每个cpu都可以同时运行着一个线程(即并发)。
每个cpu都有一组寄存器,cpu操作寄存器比操作内存的变量快得多。
每个cpu也可能有一个cpu缓存层。事实上,现在大多数cpu都有一定大小的缓存。cpu访问缓存也比主存快得多,但没有访问寄存器那么快,也就是介于两者之间。有一些cpu有多级缓存(例如我的就有三级缓存),当然这个不需要太了解,我们只需要知道cpu可以有缓存。
通常一个cpu需要访问主存数据时,它会读取部分数据倒缓存中,它也可能读取缓存的数据倒寄存器中然后操作它。当cpu需要将结果写入到内存,它会将寄存器的值刷新到缓存中,然后在某个时间将值刷新到内存中。
缓存中的值通常会在cpu需要其他的数据时刷新到主存中。cpu的缓存可以在某个时间写数据,然后在另一个时间刷新到内存。
java内存模型中的不论堆还是栈都可以在计算机这三个存储元件中,这里就因此会引发下面的一个问题:
如果有一个object(自然是在堆中)储存在主存中,一个线程运行在cpu1中,然后读取这个可以共享的object,这让他可以改变这个object,因为是首先修改了缓存的数据,当在缓存的数据并未写入到主存前,运行在其他cpu的内存就访问不到这个改变后的版本,这是因为每一个线程都有自己对这个对象的拷贝,而且拷贝到不同的cpu缓存中的。
这个问题可以用java关键字volatile解决,volatile关键词可以确保这个变量是直接从主存中读取的,而且更新后会直接写入到主存中。
资源竞争问题:
如果多个线程访问同一个对象,然后都修改了这个对象,这会引起资源竞争问题(Race conditions)
想象这样的一个场景,如果一个线程A读取一个共享的对象的变量count,线程B做了同样的动作,但是读取到不同的cpu缓存中,现在线程A对这个count加了1,线程B做了同样的动作,所以这个count同时被增长了两次但在不同的缓存中。
如果这些增长是相继的发生,那么写回到主存中的将是原有的值+1.
然而,这两个增长操作是在并未加锁下同时发生的。无论是A还是B修改后的版本写回到主存中,这个更新值都最多是加了1而不是2。
这个使用volition并不能有效的解决,因为volition只能保证是从主存中读取的,A跟B可能同时读取这个值,这时即便其中任意一个线程更新完写入到主存,另一个线程也不是修改后的值。
这可以使用java synchronized block解决,一个synchronized block在某个时间段只能被一个线程访问。同步代码块保证这个代码块中的变量都是从主存中读入的,而且当线程退出这个代码块是,所有的变量都会写入到主存中,无论这个变量声明了volatile与否。
这篇文章非我原创,是一篇英文文章我做的简单的翻译过来的,原文地址是:http://tutorials.jenkov.com/java-concurrency/java-memory-model.html。