俗话说,本身写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结以下:java
为何须要关注Java内存模型?c++
众所周知,计算机某个运算的完成不只仅依靠cpu及其寄存器,还要和内存交互!cpu须要读取内存中的运行数据,存储运算结果到内存中……其中很天然的也是没法避免的就涉及到了I/O操做,而常识告诉咱们,I/O操做和cpu的运算速度比起来,简直没得比!前者远远慢于后者(书上说相差几个数量级!),前面JVM学习2也总结了这个情景,人们解决的方案是加缓存——cache(高速缓存),cache的读写速度尽量的接近cpu运算速度,来做为内存和cpu之间的缓冲!旧的问题解决了,可是引起了新的问题!若是有多个cpu怎么办?程序员
现代操做系统都是多核心了,若是多个cpu和一块内存进行交互,那么每一个cpu都有本身的高速缓存块……咋办?也就是说,多个cpu的运算都访问了同一块内存块的话,可能致使各个cpu的缓存数据不一致!if发生了上述情景,then以哪一个cpu的缓存为主呢?为了解决这个问题,人们想到,让各个cpu在访问缓存时都遵循某事先些规定的协议!由于无规矩不成方圆!如图(如今能够回答什么是内存模型了):编程
什么是内存模型?数组
通俗的说,就是在某些事先规定的访问协议约束下,计算机处理器对内存或者高速缓存的访问过程的一种抽象!这是物理机下的东西,其实对虚拟机来讲(JVM),道理是同样的!缓存
什么是Java的内存模型(JMM)?安全
教科书这样写的:JVM规范说,Java程序在各个os平台下必须实现一次编译,处处运行的效果!故JVM规范定义了一个模型来屏蔽掉各种硬件和os之间内存访问的差别(好比Java的并发程序必须在不一样的os下运行效果是一致的)!这个模型就是Java的内存模型!简称JMM。多线程
让我通俗的说:Java内存模型定义了把JVM中的变量存储到内存和从内存中读取出变量的访问规则,这里的变量不算Java栈内的局部变量,由于Java栈是线程私有的,不存在共享问题。细节上讲,JVM中有一块主内存(不是彻底对应物理机主内存的那个概念,这里说的JVM的主内存是JVM的一部分,它主要对应Java堆中的对象实例及其相关信息的存储部分)存储了Java的全部变量。且Java的每个线程都有一个工做内存(对应Java栈),里面存放了JVM主内存中变量的值的拷贝!且Java线程的工做内存和JVM的主内存独立!如图:架构
当数据从JVM的主内存复制一份拷贝到Java线程的工做内存存储时,必须出现两个动做:并发
反过来,当数据从线程的工做内存拷贝到JVM的主内存时,也出现两个操做:
read,load,store,write的操做都是原子的,即执行期间不会被中断!可是各个原子操做之间可能会发生中断!对于普通变量,若是一个线程中那份JVM主内存变量值的拷贝更新了,并不能立刻反应在其余变量中,由于Java的每一个线程都私有一个工做内存,里面存储了该条线程须要用到的JVM主内存中的变量拷贝!(好比实例的字段信息,类型的静态变量,数组,对象……)如图:
A,B两条线程直接读or写的都是线程的工做内存!而A、B使用的数据从各自的工做内存传递到同一块JVM主内存的这个过程是有时差的,或者说是有隔离的!通俗的说他们之间看不见!也就是以前说的一个线程中的变量被修改了,是没法当即让其余线程看见的!若是须要在其余线程中当即可见,须要使用 volatile 关键字。如今引出volatile关键字:
volatile 关键字是干吗的?举例说明。
前面说了,各个线程之间的变量更新,若是想让其余线程当即可见,那么须要使用它,故volatile字段是用于线程间通信的特殊字段。每次读volatile字段都会看到其它线程写入该字段的最新值!也就是说,一旦一个共享变量(成员、静态)被volatile修饰,那么就意味着:a线程修改了该变量的值,则这个新的值对其余线程来讲,是当即可见的!先看一个例子:
这段代码会彻底运行正确么?即必定会中断么?
//线程A boolean stop = false; while(!stop){ doSomething(); } //========= //线程B stop = true;
有些人在写程序时,若是须要中断线程,可能都会采用这种办法。可是这样作是有bug的!虽然这个可能性很小,可是只要一旦bug发生,后果很严重!前面已经说了,Java的每一个线程在运行过程当中都有本身的工做内存,且Java的并发模型采用的是共享内存模型,Java线程之间的通讯老是隐式进行,整个通讯过程对程序员彻底透明,这也是为何若是编写多线程程序的Java程序员不理解隐式进行的线程之间通讯的工做机制,则极可能会遇到各类奇怪的并发问题的缘由。针对本题的A、B线程,若是他们之间通讯,画成图是这样的:
那么线程A和B须要通讯的时候,第一步A线程会将本地工做内存中的stop变量的值刷新到JVM主内存中,主内存的stop变量=false,第二步,线程B再去主内存中读取stop的拷贝,临时存储在B,此时B中工做内存的stop也为false了。当线程B更改了stop变量的值为true以后,一样也须要作相似线程A那样的工做……可是此时此刻,偏偏B还没来得及把更新以后的stop写入主存当中(前面说了各个原子操做之间能够中断),就转去作其余事情了,那么线程A因为不知道线程B对stop变量的更改,所以还会一直循环下去。这就是死循环的潜在bug!
若是stop使用了volatile修饰,会使得:
这样A获得的就是最新的正确的stop值——true。程序完美的实现了中断。不少人还认为,volatile这么好,它比锁的性能好多了!其实这不是绝对的,很片面,只能说volatile比重量级的锁(Java中线程是映射到操做系统的原生线程上的,若是要唤醒或者是阻塞一条线程须要操做系统的帮忙,这就须要从用户态转换到核心态,而状态转换须要至关长的时间……因此说syncronized关键字是java中比较重量级的操做)性能好,并且valatile万万不能代替锁,由于它不是线程安全的,既volatile修饰符没法保证对变量的任何操做都是原子的!(鉴于主要涉及了Java的并发编程,以后再开专题总结)。
什么是原子性?
在Java中,对基本数据类型的变量的操做是原子性操做,即这些操做是不可被中断的,要么执行,要么不执行。看例子:
1 int x = 10; //语句1 2 y = x; //语句2 3 x++; //语句3 4 x = x + 1; //语句4
这几个语句哪一个是原子操做?
其实只有语句1是原子性操做,其余三个语句都不是原子性操做。语句1是直接将数值10赋值给x,也就是说线程执行这个语句会直接将数值10写入到工做内存中。线程执行语句2实际上包含2个操做,它先要去主内存读取x的值,再将x的值写入工做内存,虽然读取x的值以及将x的值写入工做内存这2个操做都是原子性操做,可是合起来就不是原子性操做了。一样的,x++和 x = x+1包括3个操做:读取x的值,进行加1操做,写入新的值。因此上面4个语句只有语句1的操做具有原子性。也就是说,只有简单的读取、赋值(并且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操做)才是原子操做。
不过这里有一点须要注意:在32位平台下,对64位数据的读取和赋值是须要经过两个操做来完成的,不能保证其原子性。可是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操做了。从上面能够看出,Java内存模型只保证了基本读取和赋值是原子性操做,若是要实现更大范围操做的原子性,能够经过synchronized和Lock来实现。因为synchronized和Lock可以保证任一时刻只有一个线程执行该代码块,那么天然就不存在原子性问题了,从而保证了原子性。
什么时候使用volatile关键字?
一般来讲,使用volatile必须具有如下2个条件:
这些条件代表,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。个人理解就是上面的2个条件须要保证操做是原子性操做,才能保证使用volatile关键字的程序在并发时可以正确执行。好比boolean类型的标记变量。
前面只是大概总结了下Java的内存模式和volatile关键字,不是很深刻,留待后续并发专题补充。下面接着看几个以前和以后会遇到的概念:
到底什么是可见性?如何保证?
谈谈对指令重排的理解
a=1;
b=2;
先给a赋值,和先给b赋值,其实没什么区别,效果是同样的,这样的代码就是可重排代码,编译器会针对上下文对指令作顺序调整,哪一个顺序好,就用哪一个,因此实际上两句话怎么个执行顺序,是不必定的。
有可重排就天然会有不可重排,首先要知道Java内存模型具有一些先天的“有序性”,即不须要经过任何手段就可以保证有序性,这个一般也称为 happens-before 原则。若是两个操做的执行次序没法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机能够随意地对它们进行重排序。反之遵循了happen-before原则,JVM就没法对指令进行重排序(看起来的)。这样又引出了一个新问题:
什么是先行发生原则happens-before?
下面就来具体介绍下happens-before(先行发生原则,这里的先行和时间上先行是两码事;):
前4条规则是比较重要的,后4条规则都是常识。
好比像以下这样的线程内的串行语义()是不可重排语句:
a = 1; b = a;// 写一个变量以后,再读这个变量。
a = 1; a = 2; // 写一个变量以后,再写这个变量。
a = b; b = 1; // 读一个变量以后,再写这个变量。
以上语句不可重排,单线程的程序看起来执行的顺序是按照代码顺序执行的,这句话要正确理解:JVM实际上仍是可能会对程序代码不存在数据依赖性的指令进行指令重排序,虽然进行重排序,可是最终执行的结果是与单线程的程序顺序执行的结果一致的。所以,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但没法保证程序在多线程中执行的正确性。对于多线程环境,编译器不考虑多线程间的语义。看一个例子:
1 class OrderExample { 2 private int a = 0; 3 4 private boolean flag = false; 5 6 public void writer() { 7 a = 1; 8 flag = true; 9 } 10 11 public void reader() { 12 if (flag) { 13 int i = a + 1; 14 } 15 } 16 }
让线程A首先执行writer()方法,接着让线程B线程执行reader()方法,线程B若是看到了flag,那么就可能会当即进入if语句,可是在int i=a+1处不必定能看到a已经被赋值为1,由于在writer中,两句话顺序可能打乱!有可能对于B线程,它看A是无序的!编译器没法保证有序性。由于A彻底能够先执行flag=true,再执行a=1,不影响结果!如图:
也就是说多线程之间没法保证指令的有序性!先行发生原则的程序次序有序性原则是针对单线程的。也就是说,若是是一个线程去前后执行这两个方法,彻底是ok的!符合happens-before原则的第一条——程序次序有序性,故不存在指令重排问题。
如何解决呢?仍是套用先行发生原则,看第二条锁定原则,咱们可使用同步锁:
class OrderExample { private int a = 0; private boolean flag = false; public synchronized void writer() { a = 1; flag = true; } public synchronized void reader() { if (flag) { int i = a + 1; } } }
由于写、读都加锁了,他们之间本质是串行的,即便线程A占有写锁期间,JVM对写作了指令重排也不要紧,由于此时锁被A拿了,B线程没法执行读操做,直到A线程把写操做执行完毕,释放了该锁,B线程才能拿到这同一个对象锁,而此时,a确定是1,flag也必然是true了。此时必然是有序的。通俗的说,同步后,即便作了重排,由于互斥的缘故,reader 线程看writer线程也是顺序执行的。
其余语言(c和c++)也有内存模型么?
大部分其余的语言,像C和C++,都没有被设计成直接支持多线程。这些语言对于发生在编译器和处理器平台架构的重排序行为的保护机制会严重的依赖于程序中所使用的线程库(例如pthreads),编译器,以及代码所运行的平台所提供的保障。
最后补充下一个问题:Java的字节码两种运行方式——解释执行和编译执行