俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下:
- 为什么学习Java的内存模式
- 缓存一致性问题
- 什么是内存模型
- JMM(Java Memory Model)简介
- volatitle关键字
- 原子性
- 可见性
- 有序性
- 指令重排
- 先行发生——happen-before原则
- 解释执行和编译执行
- 其他语言(c和c++)也有内存模型么?
为什么需要关注Java内存模型?
众所周知,计算机某个运算的完成不仅仅依靠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)操作
- 由Java线程的工作内存执行相应的load操作
反过来,当数据从线程的工作内存拷贝到JVM的主内存时,也出现两个操作:
- 由Java线程的工作内存执行的存储(store)操作;
- 由JVM主内存执行的相应的写(write)操作
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修饰,会使得:
- B线程更新stop值为true,会强制将修改后的值立即写入JVM主内存,不许原子操作之间中断。
- 线程B修改stop时,也会让线程A的工作内存中的stop缓存行失效!因为A线程的工作内存中JVM主内存的stop的拷贝值缓存行无效了,所以A线程再次读取stop的值会去JVM主内存读取
这样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