目录
1. as-if-serial
2. 可见性实现原理
3. 禁止指令重排的原理分析
4. 内存屏障
5. 原理
序号 | 名称 | 链接地址 |
1 | 并发编程系列(一)创建线程的三种方式及线程如何完成上下文如何切换 | https://blog.csdn.net/qq_38130094/article/details/103443997 |
2 | 并发编程系列(二)之线程的中断 | https://blog.csdn.net/qq_38130094/article/details/103444171 |
3 | 并发系列(三)线程常用的方法 | https://blog.csdn.net/qq_38130094/article/details/103446126 |
4 | 并发编程系列(四)之Thread类源码分析(一) | https://blog.csdn.net/qq_38130094/article/details/103448160 |
5 | 并发编程系列(五)volatile关键字详解 | https://blog.csdn.net/qq_38130094/article/details/103448564 |
6 | 并发编程系列(六)volatile 之 as-if-serial 指令重排 volatile内存语义 volatile原理 | https://blog.csdn.net/qq_38130094/article/details/103543998 |
7 | 线程系列(七)synchronized使用方式 | https://blog.csdn.net/qq_38130094/article/details/103537663 |
8 | 线程系列(八)synchronized实现原理与应用 | https://blog.csdn.net/qq_38130094/article/details/103537668 |
9 | 并发编程系列(九)ThreadLocala是如何解决共享变量的并发问题及源码分析 | https://blog.csdn.net/qq_38130094/article/details/103665098 |
10 | 并发编程系列(十)AQS同步器独占锁加锁与解锁-源码解读 | https://blog.csdn.net/qq_38130094/article/details/103540315 |
11 | 并发编程系列(十一)AQS同步器共享锁加锁解锁源码解读 | https://blog.csdn.net/qq_38130094/article/details/103646505 |
12 | 并发编程系列(十二)AQS同步器条件锁(Condition)加锁解锁源码解读 | https://blog.csdn.net/qq_38130094/article/details/103679146 |
13 | 发编程系列(十三)ReentrantLock 重入锁 | https://blog.csdn.net/qq_38130094/article/details/103843779 |
14 | 发编程系列(十四)Semaphore信号量 | https://blog.csdn.net/qq_38130094/article/details/103846420 |
15 | 发编程系列(十五) CountDownLatch闭锁 | https://blog.csdn.net/qq_38130094/article/details/103855632 |
16 | 并发编程系列(十六) CyclicBarrier | https://blog.csdn.net/qq_38130094/article/details/103859704 |
1. as-if-serial
不管编译器和处理器怎么重排序,单线程的执行结果都不能被改变。编译器,运行时和处理器都必须遵守as-if-serial
2. 可见性实现原理
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。.
- 内存屏障,又称内存栅栏,是一个 CPU 指令。
- 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。
- 被volatile修饰编译后,会产生一个LOCK#前缀来对总线加锁其他CPU对内存读写阻塞(总线加锁方式)直到锁释放
- 后通过缓存一致性协议和嗅探技术来解决的;
请参照上篇文章:并发编程系列(五)volatile关键字详解(1)
3. 禁止指令重排的原理分析
被volatile 修饰的变量禁止指令重排,那这个关键字是如何禁止指令重排序的呢?
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序:
编译器优化重排序:编译器在不改变单线程语义的前提下,可以安排语句的执行顺序
指令级并行的重排序:现代处理器采用了指令级并行技术(INnstruction-Level Parallelism,ILP),如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序
内存系统的重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和储存操作看上去可能是在乱序执行
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序,【不是所有的编译器重排序都要禁止】
对于处理器重排序,JMM的处理器重排序规则会要求java编译器生成指令时插入特定类型的内存屏障【memory barriers】指令。通过内存屏障指令来禁止特定类型的处理器重排序,为程序员提供一致的内存可见性保证
JVM规范了视图定义一种JMM来屏蔽各个硬件平台和OS的内存访问差异,属于语言级的内存模型,实现让Java程序在各平台下都能达到一致的内存访问效果,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
4. 内存屏障
为了实现volatile可见性和happen-before的语义,JVM底层通过一个叫做“内存屏障”的东西来完成。内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存的顺序限制,
是否可以重排序 | 第二个操作 | |||
第一个操作 | 普通读 / 写 | volatile | volatile写 | |
普通读 / 写 | ||||
volatile读 | no | no | no | |
volatile写 | no | no |
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
JMM:JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台上,通过禁止特定类型的编译器重排序和处理器冲排序,为程序员提供了一致的内存可见性保证
volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况忽略不必要的屏障。在JMM基础中就有提到过各个处理器对各个屏障的支持度,其中x86处理器仅会对写-读操作做重排序。
5. 原理
volatile主要作用是具有可见性和原子性(单个变量),其实现原理就是利用屏障来保障实现。
要想彻底掌握就应该多做下相关场景的编码,经典的场景有:状态标记量、volatile方式的double check等。单例的双重检查