Java内存模型学习笔记

时间:2022-12-27 07:48:29

1.JMM定义了一个线程对共享变量的操作何时对其他的线程可见。

在线程中对虚拟机的主存里的共享变量进行操作的时候,由于存在其操作的并非是主存的变量,而是一个处于处理器缓冲区的一个变量的副本,那么必然存在一个刷新到主存
的这么一个过程,而这个过程可能会导致内存的不可见问题,也即处理器指令的重排序问题。

如果一个线程读取了另一个线程修改但是没有及时刷新到主存的那个变量,那么就产生了数据的不一致问题。那么java的内存模型对此的处理办法是提供一个称之为内存屏障的东西,他会保证你在执行一个操作的时候,会及时的刷新到主存,提供的四个屏障指令为LoadStore,LoadLoad,StoreStore,StoreLoad,。

java5(JSP-131)的内存模型规定了如果一个操作的执行结果对另一个操作可见,需要存在happen-before的关系,也就是说这个操作执行顺序不一定要在另一个操作之前,但是
其结果必须对另一个操作可见(常表现为刷新到了主存)。

happen-before规则可见如下:
同一个线程里面,前面的操作happen-before后面的操作
监视锁的规则:解锁happen-before于加锁
volatile变量的规则:对这个域的写操作happen-before于读操作
具有传递性

为了实现这个happen-before的目标,JMM在实现这个的时候采用了给处理器和编译器定义了重排序的规则来控制(编译器,处理器,内存系统的重排序)。

重排序的意义在于:充分利用CPU的特性来对指令的执行的顺序做优化,这中优化可能会打乱指令的执行顺序。但是同时也是遵循一定的规则:
不能打断两个具有数据依赖性的操作,但是仅仅限于同一个线程里面,同一个处理器里面。

数据依赖性:后一个操作的依赖于前一个操作的操作结果,这个不能重排序

控制依赖性:后一个操作的执行依赖于前面的条件判断的真伪,这个可以重排序,,应为重排序之后不会改变执行的结果。但是在多线程的环境下面,处理器可能会
使用一种称之为 (猜测执行) 来克服控制依赖对并行性的影响(如果条件为false,那么在这个线程所获得时间片内就无法继续做更多的事情,所以可能会先执行下面的 语句,比如预先读取变量的值,然后做运算然后存在缓存里面等到条件为真的时候看,直接把这个临时变量的值直接替代表达式,这就改变了语义使得计算结果不同了。)

as-if-serial语义遵循:不论如何重排序都不会影响执行结果。

1.1.1内存屏障指令:
LOADLOAD: load1,loadload;load2 load1装载指令会先于load2以及所有后续的装载指令的装载
STORESTORE store1,storestore,store2 store1的数据对其他处理器可见先于store2以及后续的store指令
LOADSTORE load ;laodstore;store load指令的装载先于store以及后续的store指令
STORELOAD store;storeload;load store数据对于其他的处理器可见之前于load及后续的Load指令,这个指令会使得之前的所有的store和load指令完成之后
再进行内存的访问load指令。
其中storeload的代价是最高的,也具有前面的内存指令的功能,我们在一个store指令之后,一般会直接影响的就是后续的读取指令,如果没有及时的刷新到内存中将会造成
多线程的不同步问题

1.2顺序一致性内存模型

这个概念大概说的是:在单线程中操作的顺序是按照程序的顺序来执行的,多线程的环境下,整体的执行顺序是对所有线程来说是唯一的,而且每个操作的结果都是立即
对其他线程可见的,即便这个程序不是正确同步的。

顺序一致性并不能保证正确的同步,即不能保证程序的执行顺序和所预期的顺序一致,只是保证了所有的操作结果都立即可见,即保证了所有线程看到的执行顺序都是一样的。

但是在真实的JMM中,由于处理器缓存的存在,存在一个操作结果刷新到主存的过程,那么未正确同步的程序在多线程的环境下,每个线程看到的操作执行的顺序可能是不同的,当B的操作发生时,如果A的操作发生了但是结果没有刷新到主存里,对B而言,A的这个操作就没有发生,但是对A而言,这个操作的已经发生了。
也就是说,JMM与一致性模型的区别如下:
1.JMM不提供单线程内的顺序执行,应为会产生重排序,但是提供了as-if-serial语义,也就是说,保证了执行结果的一致性。
一致性模型提供顺序的执行。
2.JMM在多线程的环境下,不提供操作结果的立即可见性,即不提供多线程环境下的顺序一致性,每个线程看见的操作结果可能不同。
顺序一致性内存模型在多线程的环境提供上述特性,但是在非正确同步的程序中只保证顺序的一致性,不保证顺序的正确性。
3.JMM不保证对64位的long,double的院子操作性。

1.3 Volatile特性:
对这个volatile变量的操作在生成字节码的时候会加入内存屏障指令来达到其内存可见性的语义:lstore,lload,sstore,sload;
这些指令保证了在执行到这些对volatile的读写操作的时候,会阻止处理器对他进行重排序,以达到这些操作的顺序执行。

Voliatile的读写操作具有所的释放和获取的内存语义(happen-before),但是也仅仅限于读写,锁可以锁住一段代码,使其具有原子性。

严格限制volatile与普通变量之间的重排序。以此来达到其与锁的释放和获取相同的语义

1.4 JMM的锁的内存语义

一个线程在获得锁的时候会把本地内存的变量置为无效,然后,直接去内存中读取该变量,当锁释放的时候,会把修改刷新到缓存。

实现锁:
ReentrantLock:

 公平锁的实现依赖于AQS同步器(AbstractQueuedSynchronizer),其内部维持了一个volatile变量state,然后通过对其的读写及读写的立即可见性来达到

锁住代码的目的。

 不公平锁的实现,也是依赖于同步器,不过函数不同,compareAndSetState方法:如果预期值和给定的值一样,那么设置同步状态为给定值,这个设置操作必须具有原子性,否则就没有意义。为了实现这点,在X86处理器中做了以下三点:
1.加lock前缀如果实在多处理器的环境下,这个前缀可以保证对内存的读-改-写的原子性。
2.禁止该指令与之前的和之后的指令重排序。
3.把写缓冲区的所有数据刷新道主存。

即与公平锁不同的是,不公平锁在获取锁的时候,使用的是compareAndSetState来更新volatile变量的值,而这个操作是具有vilatile读写的内存语义的。

java.io.concurrent包内的类都是基于这两种方式构建的,volatile变量的读/写 和compareAndSetState所具有的volatile读写内存语义的

synchronized关键字在修饰方法的时候,其flag为ACC_SYNRONIZED,
在代码内使用的时候,会生成对应的加锁和释放锁的字节码指令。

synchronized(this)
{
System.out.printLn(“”);
}
对应的是以下的字节码:
0: aload_0
1: dup
2: astore_1
3: monitorenter ————–或得锁
4: getstatic #15
rintStream;
7: invokevirtual #21
V
10: aload_1
11: monitorexit ————–释放锁
12: goto 18————直接return
15: aload_1
16: monitorexit
17: athrow
18:return

整个过程如下:一个线程先获取到了共享变量

1.5 Final域:

在处理final域的时候需要遵循的重排序规则是:
1.在构造函数对final域的写入语句不能和把这个正在构造的对象赋值给其他引用变量的操作重排序。
2.在对一个final域进行读的操作不能和读包含该final域的对象的操作重排序。

理解:对于第一点,为了保证final域的正确的初始化,我们如果在对一个包含未正确完成final初始化的对象的对象赋值给其他引用,那么其得到的引用之后
,在读取这个final域的时候,可能这个final域没有完成初始化,导致出现读取错误,这完全是出乎程序本身的意料的,是由于重排序导致的错误,所以应该
保证final的正确初始化而确定这条重排序规则。

对于第二点,我们在读取一个final域的时候保证了一定会先读取这个包含这个final域的对象引用(loadload),这个保证意义如下:读取这个对象引用之后,如果这个对象
不为null,那么这个对象一定被初始化过了,也就是说调用了构造函数并且返回了,结合第一点,这个构造函数被调用了,之后其返回为止,都会先执行final域的初始化,
这点由在return之前插入storestore指令来保证,而这也确保了,能够在多线程的环境下,得到一个正确初始化的final域。

但是这些的正确都基于一个约定,先构造函数返回之前不得把这个正在构造的对象逸出到外部去,应为这个操作可能和final域赋值操作重排序。
以上是指在final域为普通数据类型的时候,在其为引用类型的时候,需要增强这个重排序的约束。

如果final域是一个对象,那么约束如下:在构造函数里面对一个final对象进行赋值操作与构造函数外把这个被构造的对象赋值给其他的引用变量不能进行重排序。

总结:

重排序的意义:比如我们在读取两个变量:
int a=q;
int b=g;
如果先执行a时,发现其在内存中被锁住了,无法读取,那么处理器就可能先执行第二句,然后再回来执行第一局,这就发生了重排序。

我们的计算机系统里面存在硬件内存模型,其实际就是规定了对于各种情况下是否采用重排序来提高执行效率,对于四种情况读写,一共有四种不同的处理器
内存模型,TSP,PCO,RMO,PowerPC(任何情况都可能方发生重排序),而对于这几种弱的内存模型,编写程序的时候不得不熟悉这些内存模型,但是这是没有必要的,
所以,为了平衡处理器对弱内存模型的渴望和程序员对强内存模型的需要,产生了JMM这个语言级的内存模型,它的原则是,在不改变程序执行结果的情况下,允许
处理器和编译器进行各种优化。实现这个目的的手段是使用内存屏障指令来完成各种不同的处理器内存模型的平衡。

当然JMM提供的内存可见性是其直接的表现,单线程程序会被保证,其不存在内存可见性的问题,而且其执行结果和在顺序一致性的内存模型中执行结果相同。

正确同步的多线程程序会被保证,操作执行的顺序具有顺序一致性,JMM限制了编译器和处理器来完成这个内存可见性的目标。其具体的表现为,在程序员使用
volatile,synchronized,llock等特殊的同步手段或者在多线程环境线面存在内存可见性的情况通过内存屏障指令来完成各个关键字的内存语义,以此来对程序
屏蔽内存模型的细节的各个处理器内存模的差异。

当然如果程序是未同步的多线程程序,那么JMM只提供最小的安全保证,线程读取到的值,要么是之前某个线程写入的,要么就是被系统初始化过的null或者0,折
个保证的意义在于,你不会得到一个未初始化的莫名其妙的值,特别是在这个值为一个引用类型的时候。

JMM内存模型的变更:
JDK5之前的内存模型存在以下两个问题:
1.对于volatile的内存语义不严谨,允许volatile与普通变量的读写重排序,这可能会导致其

2.final的内存语义之前存在肯恩过多次读取不同的值的情况,后面增加了两条新的读写重排序规则来增加语义,保证了,如果对一个对对象是正确初始化的(引用没有
从构造函数里面逃逸)那么任何线程对这个finaL域的对象的读取都是一样的。