Deep Analysis Java Memory Model

提纲:

•Java内存模型
•volatile关键字
•long和double变量的特殊规则
•原子性,可见性与有序性
•先行发生原则
•Java与线程

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与线程

线程是比进程更轻量级的调度单位,在java API中,Thread类的所有关键方法都声明为native的。
实现线程主要有三种方式:使用内核线程实现,使用用户线程实现,使用用户线程加轻量级进程混合实现。
使用内核线程实现:内核线程(Kernel-level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上,每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情。
由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响倒这个进程继续工作。
由于基于内核线程实现的,各种线程操作(创建,析构,同步)都需要进行系统调用。系统调用成本较高,需要在User Model和Kernel Model中来回切换。LWP消耗一定内存空间,系统支持LWP的数量是有限的.
用户线程实现:
一个线程只要不是内核线程,就可以认为是用户线程(User Thread,UT)
 
使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有线程操作都需要用户程序自己处理。
除了以前在不支持多线程的操作系统中(如DOS)的多线程程序与少数有特殊需求的程序外,使用这种模式的程序越来越少.
用户线程加轻量级进程混合实现:
线程除了依赖内核线程实现和完全有用户程序自己实现之外,还有一种将内核线程和用户线程一起使用的实现方式。
UT的创建,切换,析构等操作依然廉价,并且可以支持打规模UT并发。而操作系统提供支持的LWP则作为UT和KLT之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且UT的系统调用要通过LWP来完成,降低了这个进程被完全阻塞的风险。
Java线程的实现,JDK1.2之前是基于“绿色线程”(Green Thread)的用户线程实现的。在目前的JDK版本中,操作系统支持怎样的线程模型,决定了Java虚拟机的线程是怎样映射的。
Windows版本与Linux版本都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中。Solaris平台中,由于操作系统的线程特性可以同时支持一对一及多对多的线程模型,Solaris版的JDK提供了专有的虚拟机参数(-XX:UseLWPSynchronization和-XX:UseBoundThreads)来明确指定虚拟机使用那种线程模型.
 
Java线程调度:
线程调度是指系统为线程分配处理器使用权的过程,主要调度有两种方式,分别是协同式线程调度和抢占式线程调度。
如果使用协同式调度多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上.
如果使用抢占式调度的多线程系统,那么每个线程的将由系统来分配执行时间,线程的切换不由线程本身来决定。
Java使用的线程调度方式就是抢占式调度.
 
Java语言定义了以下几种状态,在任何一个时间点,一个线程只能有且只有其中的一种状态
New;Runable;Waiting;Timed Waiting;Blocked;Terminated;
 
线程安全:
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协助操作,调用这个对象的行为都可以获得正确地结果,那这个对象时线程安全的。
在java语言中(JDK1.5之后,即Java内存模型被修改之后的Java语言),不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取额外的线程安全保障措施.
绝对的线程安全满足一个类要达到不管运行时环境如何,调用者都不需要任何额外的同步措施;
相对线程安全就是我们通常意义上的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性;
线程兼容是指对象本省不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用;
 
 
 
posted @ 2015-08-05 20:08  shuffle  阅读(480)  评论(0编辑  收藏  举报