该文章属于《Java并发编程》系列文章,若是想了解更多,请点击《Java并发编程之总目录》java
为了提升计算机处理数据的速度。现代的计算机都支持多任务处理。在32位windows操做系统中 ,多任务处理是指系统可同时运行多个进程,而每一个进程也可同时执行多个线程。一个线程是指程序的一条执行路径,它在系统指定的时间片中完成特定的功能。系统不停地在多个线程之间切换,因为时间很短,看上去多个线程在同时运行。或者对于在线程序可并行执行同时服务于多个用户称为多任务处理。程序员
在理解java内存模型以前,咱们先来了解一下,物理计算机的内存模型,其对Java内存模型有着很大的参考意义。 在物理计算机中,咱们须要处理的数据都在内存中,处理器处理数据,须要从内存中获取相应的数据,而后存入内存中,为了提升计算机的处理速度(读取数据,存储数据有IO消耗),咱们经常会在CPU(处理器)中加入高速缓存(Cache Memory),也就是将数据缓存处处理器中,当处理器处理完数据后,再将处理的数据结果存储在内存中。具体以下图所示:编程
当CPU(处理器)要读取一个数据时,首先从一级缓存中查找,若是没有找到再从二级缓存中查找,若是仍是没有就从三级缓存或内存中查找。通常来讲,每级缓存的命中率大概都在80%左右,也就是说所有数据量的80%均可以在一级缓存中找到,只剩下20%的总数据量才须要从二级缓存、三级缓存或内存中读取。windows
高速缓存(Cache Memory)是位于CPU与内存之间的临时存储器,它的容量比内存小的多可是交换速度却比内存要快得多。高速缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,由于CPU运算速度要比内存读写速度快不少,这样会使CPU花费很长时间等待数据到来或把数据写入内存。在缓存中的数据是内存中的一小部分,但这一小部分是短期内CPU即将访问的,当CPU调用大量数据时,就可先缓存中调用,从而加快读取速度。缓存
虽然高速缓缓冲提升了CPU(处理器)处理数据的速度问题。在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不一样的CPU中,所以每一个线程运行时有本身的高速缓存(对单核CPU来讲,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。这时CPU缓存中的值可能和缓存中的值不同,这就会出现缓存不一致的问题。为了解决该问题。物理机算计提供了两种方案来解决该问题。具体以下图所示:bash
总线(Bus)是计算机各类功能部件之间传送信息的公共通讯干线,它是由导线组成的传输线束,在计算机中数据是经过总线,在处理器和内存之间传递。多线程
可是因为在锁住总线期间,其余CPU没法访问内存,会致使效率低下。所以出现了第二种解决方案,经过缓存一致性协议来解决缓存一致性问题。最出名的就是Intel 的MESI协议,MESI协议保证了每一个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,若是发现操做的变量是共享变量,即在其余CPU中也存在该变量的副本,会发出信号通知其余CPU将该变量的缓存行置为无效状态,所以当其余CPU须要读取这个变量时,发现本身缓存中缓存该变量的缓存行是无效的,那么它就会从内存从新读取。并发
除了使用高速缓存来提升CPU(处理器)的数据处理速度,CPU(处理器)还采用了容许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。在这期间不按规定顺序执行指令,而后由从新排列单元将各执行单元结果按指令顺序从新排列。采用乱序执行技术的目的是为了使CPU内部电路满负荷运转并相应提升了CPU的运行程序的速度。有可能你们很差理解。下面这个例子帮助你们理解。app
假如请A、B、C三个名人为晚会题写横幅“春节联欢晚会”六个大字,每人各写两个字。若是这时在一张大纸上按顺序由A写好"春节"后再交给B写"联欢",而后再由C写"晚会",那么这样在A写的时候,B和C必须等待,而在B写的时候C仍然要等待而A已经没事了。函数
但若是采用三我的分别用三张纸同时写的作法, 那么B和C都没必要须等待就能够同时各写各的了,甚至C和B还能够比A先写好也不要紧(就象乱序执行),但当他们都写完后就必须从新在横幅上(天然能够由别人作,就象CPU中乱序执行后的从新排列单元)按"春节联欢晚会"的顺序排好才能挂出去。
看到这里你们必定会发现,咱们所讨论的CPU高速缓存、指令重排序等内容都是计算机体系结构方面的东西,并非Java语言所特有的。事实上,不少主流程序语言(如C/C++)都存在缓存不一致的问题,这些语言是借助物理硬件和操做系统的内存模型来处理缓存不一致问题的,所以不一样平台上内存模型的差别,会影响到程序的执行结果。Java虚拟机规范定义了本身的内存模型JMM(Java Memory Model)来屏蔽掉不一样硬件和操做系统的内存模型差别,以实现让Java程序在各类平台下都能达到一致的内存访问结果。因此对于Java程序员,无需了解底层硬件和操做系统内存模型的知识,只要关注Java本身的内存模型,就可以解决这些问题啦。
Java内存模型以下图所示:
若是对应与Java内存中堆与栈的概念的话,主内存对应Java内存中的堆,工做内存对应Java虚拟机的栈。
主内存与工做内存之间的内存交互,也就是从线程的私有内存数据同步到主内存中,从主内存的读取数据到线程的私有内存中。Java内存模型定义了8种操做来完成。虚拟机在实现时保证下面提到的每一种操做都是原子的,不可再分的。
既然Java内存模型规定了内存之间交互的一些操做。那么咱们来看看,它到底拥有哪些规则呢。
上述规则规定了Java内存之间交互的流程。保证了数据在单线程情形下传输过程当中的准确性与数据一致性。
前面提到过,CPU(处理器)为了提升处理数据的速度,会进行乱序执行(out-of-orderexecution)。也就是重排序。可是CPU不会对任务操做进行重排序,编译器与处理器只会对没有数据依赖性的指令进行重排序。这里提到了一个关键词数据依赖性。什么是数据依赖呢?
若是两个操做访问同一个变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。以下图所示:
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a=1;b=a | 写一个变量以后,再读这个位置 |
写后写 | a=1;a=2 | 写一个变量以后,再写这个位置 |
读后写 | a=b;b=1 | 读一个变量以后,再写这个位置 |
上述三种状况,a与b存在着**“数据依赖性”**,同时你们也要注意。这里所说的数据依赖性是指单个处理器执行的指令序列和单个线程中执行的操做。多处理器和不一样线程之间是没有数据依赖性这种关系的。
既然咱们已经知道了CPU在处理数据时候会出现重排序。那重排序的规则是什么呢?重排序规则:无论怎么重排序(编译器和处理器为了提升并行度),单线程(程序)执行结果不能被改变。编译器、runtime和处理器都必须遵照。那么咱们三角形面积示例代码说明:
double a = 3;//底
double h = 10;//高
double s = a*h/2//面积
复制代码
其中上述代码的依赖关系以下图所示:
前面咱们已经了解了Java内存模型的大体结构与操做方式,那么咱们来看看Java内存模型须要解决的问题。
工做内存的可见性问题(这里和计算机硬件的缓存不一致是同样的道理)。从上文的Java内存模型分析。咱们已经知道了当多个线程操做同一个共享变量时,若是一个线程修改了其中的变量的值(若是经过Java内存模型的原子操做来表达,一个线程屡次use与assign 操做,而另外一个线程通过read、load以后,另外一线程任然保持着以前从主内存中获取的值),另外一个线程怎么感知呢?
CPU(处理器)的重排序会对多线程带来问题。具体问题咱们用下列伪代码来阐述:
public class Demo {
private int a = 0;
private boolean isInit = false;
private Config config;
public void init() {
config = readConfig();//1
isInit = true;//2
}
public void doSomething() {
if (isInit) {//3
doSomethingWithconfig();//4
}
}
}
复制代码
isInit用来标志是否已经初始化配置。其中1,2操做是没有数据依赖性,同理三、4操做也是没有数据依赖性的。那么CPU(处理器)可能对一、2操做进行重排序。对三、4操做进行重排序。如今咱们加入线程A操做Init()方法,线程B操做doSomething()方法,那么咱们看看重排序对多线程状况下的影响。
上图中2操做排在了1操做前面。当CPU时间片转到线程B。线程B判断 if (isInit)为true,接下来接着执行 doSomethingWithconfig(),可是咱们Config尚未初始化。因此在多线程的状况下。重排序会影响程序的执行结果。
上面咱们讨论了Java内存模型须要解决的问题,那Java有不有一个良好的解决办法来处理以上出现的状况呢?答案是固然的。为了方便程序员开发,将底层的烦琐细节屏蔽掉,JMM定义了Happens-Before原则。只要咱们理解了Happens-Before原则,无需了解Java内存模型的内存操做,就能够解决这些问题(避免工做内存的不可见与重排序带来的问题)。
Happens-Before原则是一组偏序关系:对于两个操做A和B,这两个操做能够在不一样的线程中执行。若是A Happens-Before B,那么能够保证,当A操做执行完后,A操做的执行结果对B操做是可见的。那么有哪些知足Happens-Before原则的呢?下面是Java内存模型规定的一些规则。
在一个线程内,按照程序代码顺序,书写在前面的操做先行发生于书写在后面的操做。这是由于Java语言规范要求Java内存模型在单个线程内部要维护相似严格串行的语义,若是多个操做之间有前后依赖关系,则不容许对这些操做进行重排序。
对一个unlock操做先行发生于后面对同一个锁的lock操做。
public class Demo {
private int value;
public synchronized void setValue(int value) {
this.value = value;
}
public synchronized int getValue() {
return value;
}
}
复制代码
上面这段代码,setValue与getValue拥有同一个锁(也就是当前实例对象),假设setValue方法在线程A中执行,getValue方法在线程B中执行。线程A调用setValue方法会先对value变量赋值,而后释放锁。线程B调用getValue方法会先获取到同一个锁后,再读取value的值。那么B线程获取的value的值必定是正确的。
对一个volatile变量的写操做先行发生于后面这个变量的读操做。
public class Demo {
private volatile boolean flag;
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean isFlag() {
return flag;
}
}
复制代码
上面这段代码,假设setFlag方法在线程A中执行,isFlag方法在线程B中执行。线程A调用setFlag方法会先对value变量赋值,而后释放锁。线程B调用isFlag方法再读取value的值。那么B线程获取的flag的值必定是正确的。这里咱们先不对volatlie进行讲解,后面系列文章会描述。
Thread对象的start()方法先行发生于此线程的每一个动做。
start方法和新线程中的动做必定是在两个不一样的线程中执行。线程启动规则能够这样去理解:调用start方法时,会将start方法以前全部操做的结果同步到主内存中,新线程建立好后,须要从主内存获取数据。这样在start方法调用以前的全部操做结果对于新建立的线程都是可见的。
线程中的全部操做都先行发生于对此线程的终止检测。
这里理解比较抽象。举个例子,假设两个线程s、t。在线程s中调用t.join()方法。则线程s会被挂起,等待t线程运行结束才能恢复执行。当t.join()成功返回时,s线程就知道t线程已经结束了。在t线程中对共享变量的修改,对s线程都是可见的。相似的还有Thread.isAlive方法也能够检测到一个线程是否结束。也就是说当一个线程结束时,会把本身全部操做的结果都同步到主内存。而任何其它线程当发现这个线程已经执行结束了,就会从主内存中从新刷新最新的变量值。因此结束的线程A对共享变量的修改,对于其它检测了A线程是否结束的线程是可见的。
对线程interrupt()方法的调用先与被中断线程的代码检查到中断事件的发生。
假设两个线程A和B,A先作了一些操做operationA,而后调用B线程的interrupt方法。当B线程感知到本身的中断标识被设置时(经过抛出InterruptedException,或调用interrupted和isInterrupted),operationA中的操做结果对B都是可见的。
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
若是操做A先行与发生于操做B,操做B先行发生于操做C,那么就能够得出A先行发生于操做C的结论。