? 在介绍Java内存模型之前,我们先了解一下物理计算机中的并发问题,因为物理机遇到的并发问题与虚拟机中的情况有不少相似之处。
? 由于内存与cpu的缓存运算速度有几个数量级的差距,所以如果将运算数据加载到cpu的缓存中运算提高运算速度,然后结束后再从缓存同步回内存之中。可是这样带来了一个问题 缓存一致性。每个处理器核心都有自己的高速缓存,处理变量时会拷贝一份主存中的变量到各自核心的高速缓存中。当各自核心处理任务时。将会导致各自的缓存数据不一致。为此,要制定一些协议,各个核心在读写时要根据协议进行操作,来维护缓存的一致性。
? cpu为了提高运算效率,会对代码进行乱序执行,然后对乱序执行的结果重组。这与代码顺序执行的结果是一样的。可是在一个核的计算任务依赖另一个核计的算任务的中间结果,那么处理器最终得出的结果和我们期望得到的结果可能会大不相同。
? 可以理解Java内存模型就是上述问题的抽象。多个cpu核心抽象成多个线程,主内存抽象成Java的主内存,cpu高速缓存抽象成Java线程的各自内存。 这样我们就好理解了。
主内存:Java内存模型规定了所有变量都存储在主内存( 每条线程都有自己的工作内存)
工作内存:每条线程都有自己的工作内存,又称本地内存,线程的工作内存中保存了该线程使用到的变量在主内存中的共享变量的副本拷贝。
和前面的物理机介绍类似,下面介绍的Java内存模型的执行处理将围绕解决这2个问题展开:
1.各个线程的工作内存是主内存中的共享变量副本,当多个线程的运算任务都涉及同一个共享变量时,将导致各自的的共享变量副本不一致,数据同步回主内存以谁的副本数据为准? Java内存模型主要通过一系列的数据同步协议、规则来保证数据的一致性。这个后面介绍
2.指令重排问题,编译器为了优化程序性能而对指令进行重新排序执行。多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令重排序导致运行结果与预期不同
为了解决多线程环境下共享变量的一致性和指令重排问题。Java内存模型定义了三大特性:原子性、可见性、有序性来保证共享变量的一致性。定义先行发生原则(Happens-Before)和内存屏障解决指令重排的有序性问题。在看这两个问题之前我们先熟悉下内存间的交互操作。
关于主内存与工作内存之间具体的交互协议,Java内存模型定义了以下8种原子的具体的操作来完成:lock,unlock,read,load,use,assign,store,write。
小总结:把一个变量从主内存复制到工作内存,那就要按顺序地执行read和load操作。把一个变量从工作内存同步回主内存,就要按顺序地执行store和write操作。注意这里的按顺序没说要连续。可以在read与load之间、store与write之间插入其它操作。比如,对主内存中的变量a和b的访问,可以按照以下顺序执行:read a -> read b -> load b -> load a。
另外,Java内存模型还定义了执行上述8种操作的基本规则:
注意,这里的lock和unlock是实现synchronized的基础,Java并没有把lock和unlock操作直接开放给用户使用,但是却提供了两个更高层次的指令来隐式地使用这两个操作,即moniterenter和moniterexit。
原子性(Atomicity) 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行 。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
由Java内存模型来直接保证的原子性操作包括read、load、user、assign、store、write这些操作,我们可以大致认为基本类型变量的读写是具备原子性的。如果应用需要一个更大范围的原子性,Java内存模型还提供了lock和unlock这两个操作来满足这种需求,尽管不能直接使用这两个操作,但我们可以使用它们更具体的实现synchronized来实现。
可见性(Visibility) 是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 。
? Java内存模型是通过在变更修改后同步回主内存, 依赖主内存作为传递媒介的方式来实现可见性。无论是普通变量还是volatile变量都是如此。普通变量与volatile变量的主要区别是是否会在修改之后立即同步回主内存,以及是否在每次读取前立即从主内存刷新。volatile变量保证了多线程环境下变量的可见性,除了volatile之外,还有两个关键字也可以保证可见性,它们是synchronized和final。
? synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作”这条规则获取的。
? final的可见性是指被final修饰的字段在构造器中一旦被初始化完成,那么其它线程中就能看见这个final字段了。
有序性(Ordering)有序性规则表现在以下两种场景: 线程内和线程间,线程内从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。线程间这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(synchronized关键字修饰)以及volatile字段的操作仍维持相对有序。
? 先行发生,是指操作A先行发生于操作B,先行发生的动作的结果一定对于后面的发生的动作可见,这种影响包括修改了共享内存中变量的值、发送了消息、调用了方法等。
程序次序原则
在一个线程内,按照程序书写的顺序执行,书写在前面的操作先行发生于书写在后面的操作,准确地讲是控制流顺序而不是代码顺序,因为要考虑分支、循环等情况。
监视器锁定原则
一个unlock操作先行发生于后面对同一个锁的lock操作。
volatile原则
对一个volatile变量的写操作先行发生于后面对该变量的读操作。
线程启动原则
对线程的start()操作先行发生于线程内的任何操作。
线程终止原则
线程中的所有操作先行发生于检测到线程终止,可以通过Thread.join()、Thread.isAlive()的返回值检测线程是否已经终止。
线程中断原则
对线程的interrupt()的调用先行发生于线程的代码中检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否发生中断。
对象终结原则
一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize()方法的开始。
传递性原则
如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。这里说的“先行发生”与“时间上的先发生”没有必然的关系。
总结:
深入理解Java内存模型JMM(Java Memory Model)
原文:https://www.cnblogs.com/gaoweiBlog/p/14850947.html