基础知识
内存屏障
- 硬件层的内存屏障分为两种:
Load Barrier和Store Barrier即读屏障和写屏障。 - 内存屏障有三个作用:
- 阻止屏障两侧的指令重排序;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。(缓存一致性(窥探技术 + MESI协议 ))
对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
java内存屏障
java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
内存屏障是一组处理器指令,它并不由JVM直接暴露,因此JVM会根据不同的操作系统插入不同的指令以达成我们所要内存屏障效果。
重排序
为了提升性能,编译器和处理器会进行重排序
重排序会改变多线程程序的执行结果
1-2重排
此时线程B拿到的a是0
这是一个非同步程序
- 不保证单线程内的操作会按程序的顺序执行(重排序)
- 不保证所有线程能看到一致的操作执行顺序(不是一个原子操作)
- 不保证64位的long型和double型变量的写操作具备原子性(32位系统)
volatile的内存语义
volatile的特性
理解volatile特性的一个好办法是把对volatile变量的单个读/写操作,看成是使用同一个锁对这个单个读/写操作做了同步。
volatile变量自身具有下列特性:
- ① 可见性/一致性:对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
- ② 原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++这种复合操作不具有原子性。
volatile写/读的happens-before关系
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
volatile写/读的内存语义
-
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
-
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
volatile内存语义的实现原理
为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。因为内存屏障是一组处理器指令,它并不由JVM直接暴露,因此JVM会根据不同的操作系统插入不同的指令以达成我们所要内存屏障效果。
上述volatile读/写的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile读写的内存语义,编译器可以根据具体情况省略不必要的屏障。
volatile和锁的区别
- volatile:单个 volatile 变量的读/写具有原子性
- 锁:锁的互斥特性可以使得在整个临界区代码的执行具备原子性
锁的内存语义
锁的释放-获取的happens-before关系
线程A释放锁,线程B才能获取锁执行同步语句块,此时线程B可以看到线程A释放锁之前的操作
锁的释放和获取的内存语义
- 当线程释放锁时,JMM会把该线程对应当本地内存中的共享变量刷新到主内存中
- 当线程获取锁时,JVM会把该线程的对应本地内存置为无效,从而使被监视器保护的临界区代码必须从主内存读取共享变量
线程释放锁,到另一个线程获取锁。实质上锁线程A通过主内存向线程B发送消息
锁内存语义的实现
以ReentrantLock为例,ReentrantLock的实现依赖于同步框架AbstractQueuedSynchronizer(AQS),AQS使用一个整型的volatile(命名为state)来维护同步状态。
公平锁的内存语义实现 公平锁在释放锁的最后写一个volatile变量state,获取锁时首先读这个volatile变量。根据volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。所以释放锁的线程 happens-before获取锁的线程。
非公平锁的内存语义实现 非公平锁的释放和公平锁一样,获取的锁时会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义