标签:
提纲:
1.java内存模型
Java虚拟机规范中试图定义一种java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果.
主内存与工作内存:
JMM的主要目标是定义了程序中各个变量的访问规则,及虚拟机中将变量存储到内存和从内存中取出变量的底层细节。
备注:此处的变量与java编程时所说的变量参数有所区别,它包括了实例字段,静态字段和构成数组对象的元素,但不包括局部变量和方法参数,以为后者是线程私有的,不会被共享。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程线程使用到的变量主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
这里的主内存,工作内存与Java内存区域中的堆,栈,方法区等并不是同一个层次的内存划分。
内存间交互操作:
Java内存模型中定义了8种操作来完成,虚拟机实现时必须保证这8种操作都是原子的,不可再分的(对于double和long类型的变量来说,load,store,read,write操作在某些平台上允许有例外)。
内存间交互操作 – 8种基本操作:Lock(锁定)Unlock(解锁)Read(读取)Load(载入)Use(使用)Assign(赋值)Store(存储)Write(写入)。
内存间交互操作 – 规则:
不允许read和load,store和write操作之一单独出现;
不允许一个线程丢弃它的最近的assign操作;
不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中;
一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量;
一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复多次执行,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁;
如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始变量的值;
如果一个变量事先没有被lock操作锁定,那就不需要对其执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量;
对一个变量执行unlock操作前,必须下把此变量同步回主内存中(执行store,write操作);
2.Volatile
当一个变量被定义为volatile之后,将具备两种特性,第一是保证此变量对所有线程的可见性(这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的)
Volatile 使用原则:
运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
变量不需要与其他的状态变量共同参与不变约束;
满足上述规则的运算场景,使用volatile可以不用通过加锁来保证原子性
使用volatile变量的第二个语义是禁止指令重排序(volatile屏蔽指令重排序的语义在JDK1.5之后才被完全修复,此前的JDK中,即使将变量声明为volatile也仍然不能完全避免重排序所导致的问题(主要是在volatile变量前后的代码仍然存在重排序的问题),这也是JDK1.5之前的Java中无法安全的使用DCL来实现单例的原因)
在这里举一个简单的例子.
本文用单例模式来简单介绍一下
这个是最常见的一种模式,double check,但是为什么要在instance加上volatile关键字呢?
这个涉及到了一个类的初始化过程,类的初始话过程简单的可以分为三步,类对象初始化,类对象在堆中分配内存,堆中分配的内存指向类对象.new对象的操作,在指令层面上并不是一个操作.而且指令操作是无顺序的,不能保证哪个指令先执行.当指令堆中分配的内存指向类对象,这个对象指向就不为null,但是这个对象是否完成初始化,并无法保证.
(备注:通过对比发现,关键变化在于有volatile修饰的变量,赋值后(前面mov %eax,0x150(%esi)这句便是赋值操作)多执行了一个“lock addl $0x0,(%esp)”操作,这个操作相当于一个内存屏障,只有一个CPU访问内存时,并不需要内存屏障;但如果有两个或更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。指令“addl $0x0,(%esp)”显然是一个空操作,关键在于lock前缀,查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。)
在单线程里,java的先行原则(本文下面会有介绍)保证了,代码的执行顺序,按照代码的流程进行下去.但是在多线程里,java的先行原则无法得到保证.上面的单例模式,instance==null在多线程里,无法确定instance是否new成功.可能instance对象指向了堆中分配的内存,但是还没有执行初始化.所以在第二个判断instance==null,instance仍有可能没有完成new操作.但是加了volatile关键字后,能够保证instance在new成功之后,才被其它线程访问到.
3.long和double变量的特殊规则:
允许虚拟机实现选择可以不保证64位数据类型的load,read,store,write这四个操作的原子性,即long和double的非原子性协定(备注:实际开发中,目前个平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,不需要将long和double类型变量专门声明为volatile)
原子性,可见性和有序性:
原子性:基本数据类型的访问读写是具备原子性的(例外:long和double)。
可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
有序性:在本线程内观察,所有操作都是有序的,如果一个线程观察另一个线程,所有操作都是无序的。
4.先行发生原则(happens-before)
判断数据是否存在竞争,线程是否安全的主要依据.
1.程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
2.管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
3.volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
4.线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
5.线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
6.线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
7.对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
8.传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C
5.Java与线程
Deep Analysis Java Memory Model
标签:
原文地址:http://www.cnblogs.com/lizilong/p/4704068.html