缓存一致性问题及协议
如果A操作改a,B操作也改a,那么需要遵守缓存一致性协议来更新内存。
编译器重排序
编译器会对指令进行重排序,使运行更快,如果两个操作都对A相加,那么可以放在一起操作。也有可能对有数据依赖的操作进行重排序, 但需要满足以下两种条件.
· 单线程情况下,不能改变程序执行结果
· 存在数据依赖性关系的操作之间不能重排序
重排序的原因:
更好的使用寄存器以及现代处理器的流水线技术,减少汇编指令的数量,降低程序执行需要的CPU周期,减少CPU读写主存的时间
内存屏障
首先, 编译器并不会对数据依赖动刀,但会忽略掉控制依赖进行重排序,在高并发情况下造成问题, 有时候我们需要禁止重排序地发生。所以Java会在加入指令序列的时候存入一个内存屏障来禁止特定类型进入重排序。
控制依赖: i的结果取决于flag是不是true, 但是编译器有可能会对i进行提前计算. 跳过控制依赖.
作用:
1.禁止重排序
2.强制把存在缓存中的数据放入内存.
种类:
临界区
从获取锁到释放锁的代码段成为临界区,临界区内的代码段是可以被重排序的,但临界区内的代码不能逃出临界区
这也就解释了为什么双检索Singleton没法保证是安全的,
因为如果走的是1 3 2, 当第二个线程进来的时候,就会发现singleDcl不为空,这样的话就会直接返回一个null.
用户可能并不在乎重排序,但是要保证代码能正常运行。
Happens-Before
Happens-Before: 保证正确同步下的多线程程序执行结果不会改变。
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。(对程序员来说)
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序是允许的(对编译器和处理器来说)
Happens-Before的具体七条规则不看了,应该不会被问到。
Volatile详解
可以把volatile的读/写,看成使用同一个锁对读/写进行了同步。
可见性: 对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性: 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
Volatile的内存语义
写:在写一个Volatile变量时, JMM会把该线程对应本地内存中的变量刷新到主内存. 虽然a不是volatile, 但是也会被刷新到内存,只要a在flag赋值之前。
读:在读的时候会把共享内存会刷到线程中的主内存中, 但程序有可能不会去读一些不用的内存。
Volatile重排序
如果第一个操作是普通读/写, 第二个操作是volatile写,那么普通读写无法放到volatile写后面去进行重拍。ex...
内存屏障插入策略
· 当第二个操作是 volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。(这样数据就会一起被刷到主内存中)
· 当第一个操作是 volatile 读时,不管第二个操作是什么,都不能重排序。这 个规则确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。(这样不会提前读数据,保证先把主内存的数据刷到缓存中之后,再读数据)
· 在每个Volatile写操作前面插入StoreStore, 在每个Volatile写后面加入StoreLoad操作
· 在每个Volatile读操作后面插入一个LoadLoad屏障,在每个volatile读操作后面加入一个loadstore屏障。
实现原理
内存屏障只是Java虚拟机提出来的一个概念,而真正在CPU中实现的指令是由Lock前缀指令实现的。
unsafe.cpp lock: 1.可以对CPU中的高速缓存进行加锁,2.将缓存行数据刷回到内存中去,同时使其他CPU中储存了这个缓存数据的相关缓存无效。实现内存屏障。
网友评论