JMM和volatile,final,锁的内存语义

volatile内存语义

  • 结果可见性。对于volatile的读写都是看成直接对mem的读写,而不是对本地cache的读写。

volatile语义的实现。

  • 禁止编译器的相关重排序
  • 禁止CPU指令重排序

这两步需同时保证,否则无法保证volatile语义。对于编译器所禁止的指令重排序:

  • rw ordinary 在前, read volatile在后可以重排序
  • write volatile在前,rw ordinary在后,可以重排序

其他情况均不可重排序。为了方便记忆,可以认为对volatile的读写前后都是不可以重排序的。一般编程的时候问题不大。
对于前后都是volatile的read和write的四种组合显然都是不能重排序的。对于read volatile在前,rw ordinary在后为什么不能重排序?因为按照volatile语义,read读取的是最新的变量,那么后续的读写不可能比readvolatile读取到更老的变量。这种指令顺序隐含一种顺序,但是如果重排序之后,就打破了这种隐含的顺序。
同样,write volatile暗含:此指令之前对普通变量的读写操作都已经生效。如果重排序,则失去这种语义。

编译器可以hardcode知道如何生成不违反上述重排序规则的指令序列,但是仍然需要进一步禁止CPU的指令重排。CPU有指令流水,可能会重排指令,CPU也提供指定不可重排的接口,例如lock指令。在Java中就是四种屏障指令。storestore,storeload,loadstore,loadload。他们分别表示该指令前后的特定类型指令不可跨越该指令,即CPU不可重排序。例如,loadstore表示该fence指令前的load和该指令之后的store指令不可重排序,该指令的插入位置是load指令的下一条和sore指令的上一条。
(编译器重排序是大范围的,CPU重排序是局部小范围的)

参考解读《Java并发编程的艺术》

锁的内存语义

获取锁之后,所有发生在获取锁之前的结果,可见。释放锁之后,所以发生在释放锁之前的操作结果,可见。

这个和volatile的读写是相同的内存语义,而Lock接口的实现正式通过对于volatile状态量的读写完成的,所以,如果状态量不是一个volatile的,则无法保证锁应该具有的内存语义。

注意Object Monitor方式的锁,synchronized关键字,也效果类似,并不是规定该Object的所有域都被刷新到内存,而是锁保护的临界区代码对内存做出的改变,对释放锁之后的所有后续操作可见。

小的疑问,如果程序员使用锁作为同步工具的话,自然能够保障final的内存语义,如果不用,那写并发程序就是错的,那为什么要为final赋予特定的内存语义?

错。不适用锁写并发不一定是错的,考虑一个线程通过轮询一个变量是否为null,然后使用该变量的成员域。所以为final赋予特定内存语义是有意义的。

final域的内存语义

  • final域被初始化,构造方法返回的对象引用赋值给变量,这两者之间不可以CPU重排序

实现:通过在写final域的指令后和构造方法返回前插入storestore屏障,这样保证写final域的指令不会重排序到构造方法返回之后。

  • 读具有final域的对象,读final域,这两个指令不可以重排序。

实现:在读final域指令之前添加loadload barrier,禁止读final域的指令重排序到前面读对象引用指令之前。

  • final域如果是对象,那么构造方法中对该对象成员的写指令也不会重排序到写外层obj引用的指令之后。即final在内存语义上传递了。

注意volatile类似情况下不传递。

final语义是在JSR-133中增强了。因为增强之前可能看到final域表现为两个值:读取到初始化前后两个值不同,但是都可以被特定写法的代码读取到。注意,有一种提前“逸出”obj引用的构造方法写法,这是应该避免的,这不属于正确构造。

总结

涉及了多次禁止编译器重排序和CPU的指令重排序,只要禁止了重排序就能实现volatile语义吗?不是,这是一个前提,除此之外,在对volatile读写的时候需要把本地数据置为无效,从而flush cache到mem和从mem load到cache。

posted on 2018-05-15 10:34  还好可以改名字  阅读(681)  评论(0编辑  收藏  举报