内容导航:
l Java内存模型
l 硬件存储体系结构
l Java内存模型和硬件存储体系之间的桥梁:
u 共享对象的可见性
u 竞争条件
Java内存模型规定了JVM如何与计算机存储系统(RAM)协调工作。JVM是一个虚拟机模型,因此这个模型自然包括一个内存的模型
理解java内存模型对于设计正确的并发程序很重要。JVM规定了不同线程何时以及如何能看到那些被共享变量的读写,如何同步对共享变量的访问控制。
最初的java内存模型并不完善,所以他在java1.5中被修改了。下面的内存模型在java1.8中仍然使用。
Java内存模型
JVM中内存模型被划分为栈(stack)和堆(heap),下图是内存的逻辑图
在JVM中运行的每一个线程有自己的栈空间。每个线程栈包含有线程调用方法当前执行位置的指针。我们把它叫做栈指针。当线程执行他的代码时候,栈指针会改变。共享一块堆空间。
线程栈也包含所有正在被执行的方法的局部变量。一个线程只能访问他自己的栈。每一个线程创建的局部变量对其他线程是不可见的。纵使两个线程正在执行同一段代码,他们也只会创建属于自己栈空间的局部变量。也就是说每个线程有他自己的局部变量,相互之间不影响。
所有的局部基本数据类型(boolean、byte、short、char、int、long、float、double)完全存储在线程栈空间里,并且其他线程不可见。一个线程可以传递自己局部基本数据的拷贝给另一个线程,但是他们之间并不共享。
堆空间中存放的是应用程序中所有的对象,不管是哪一个线程创建的。并且包括基本数据对象(eg.Byte,Integer,Long等)。不管这个对象是作为局部变量被创建,还是作为另一个对象的成员变量被创建,他都在堆中。即一切对象都在堆空间。
看下图:
一个局部变量可能是基本数据类型,还可能是一个对象的引用,他们都在创建自己的线程栈空间,引用所指向的真正的对象在堆空间。
一个对象的成员方法,并且这些方法中也可能包含局部变量,这些局部变量在所属的线程栈中
一个对象的成员变量,不管是基本类型还是对象的引用,都存在这个堆空间的对象的内部
同一个对象可以被不同的拥有这个对象的引用的线程访问,并且可以访问对象的成员变量。
如果两个线程同一时刻调用相同对象的成员方法,他们都能访问对象的成员变量,但是各自有自己的局部变量的拷贝。
下面的图阐述了上述内容:
硬件存储体系结构:
下面是简单的现代计算机的存储结构:
现代计算机通常有2个甚至更多的CPU,这些CPU中的一些还可能是多核的。关键点就是多CPU的计算机允许多个线程同时运行(是真正的并行而非并发),每一个cpu在给定的时间片运行一个线程。如果java程序是多线程的,每个线程就可以在各个cpu上同时运行。
每个cpu都包含有一系列寄存器。cpu直接从寄存器执行运算比从内存要快很多倍。
每一个cpu还可能有cpu缓存(我们熟称的cache)。实际上大多数现代cpu都有不同容量的cache。Cpu从cache存取又比从内存快,但是比寄存器又慢一些。一些cpu还有多级缓存(L1、L2等),但是这并不影响理解java内存模型,我们所要知道的就是在内存和cpu之间还有一层缓存cache。
通常,cpu访问主存的时候需要读一部分到cache,还可能把cache的一部分有读到寄存器,然后执行运算。当cpu需要把运算结果写回到主存的时候,会先把值从寄存器更新到cache,cache放满后再写回主存。
存储在cache中的值通常在cpu需要存储其他的东西时一起被写回到内存。Cache把里面存储的内容一次性写回到主存。这个过程并不一定读写整个cache,通常情况下是更新cache中更小的单位,叫做cache lines 缓存行。一行或多行cache line会从主存读取到cache,或者从cache被写回主存。
Java内存模型和硬件存储体系之间的桥梁
正如已经提及的,java内存模型和硬件存储体系是不同的。硬件存储体系并不会区分堆和栈。
在硬件层面上,堆和栈都分配在主存中。堆和栈中的一部分还可能在cache和寄存器中,如下图:
当对象和变量被存储在计算机不同的存储区域(rejister、cache、main-memory)的时候,就会出现问题了。有两个问题如下:
l 线程对共享变量读写的可见性
l 读写检查共享变量的竞争条件
共享对象的可见性
如果两个或者多个线程共享一个对象而没有用volatile声明或者synchronize同步,某一个线程对共享对象的更新对其他线程可能是不可见的。
设想一下一个开始被存在主内存的共享对象。运行在cpu1上的线程把这个共享对象读入到cache。然后对这个共享对象做更改。只要cache还没有被刷新到主存,对象的更改版本对运行在其他cpu的线程就是不可见的。这样一来,每个线程就使用的是自己对共享对象的拷贝,这些拷贝存储在各自cpu cache中。
下面这幅图阐述了这种情况。一个运行在左侧cpu的线程把共享对象拷贝到自己的cpu cache中,并且把count值改为2.这个改变对运行在右侧cpu的线程是不可见的。因为对count的更新并没有刷新到主存。
要解决这个问题,我们可以使用java的volatile关键字。Volatile关键字能够确保被声明的变量直接从主存读取或者是直接在主存更新而不经过cache中间层。
竞争条件
多个线程对共享变量的更新还会引发竞争。
设想线程A读取共享变量的count字段到他的cache,线程B也是。现在线程A对count加1,线程B也是。现在var1已经被增加两次,每个cpu一次。
如果这些增加是被顺序执行(先后次序:即A read increament writeback Bread increament writeback)的,那么变量count将会顺序加1两次,最终结果是原始值加2写回主存。
但是,如果两次增加被并发执行(交叉次序)而没有适当的同步,那么不管是A还是B写回到主存的结果都是原始值加1,尽管做了两次加。
下面是上述问题的图:
为了解决这个问题,引入java的synchronized block,让某一系列操作成为原子性的,即不可以被打断(类似数据库中的事务transaction),从而实现不同线程对某一代码块的互斥访问。
翻译原文地址:http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
原文地址:http://blog.csdn.net/u014659656/article/details/44803487