🏳️🌈个人网站:code宝藏 👈,欢迎访问
🎉我的公众号:code宝藏 👨💻,分享自己的学习资源,欢迎关注🌻
🤝非常感谢大家的支持与点赞👍
有序性问题
为了提高性能,在遵守 as-if-serial 语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。
一般重排序可以分为如下三种类型:
● 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
● 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
● 内存系统重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:
可见性问题
Java 内存模型抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。Java 内存模型主要目的是为了屏蔽系统和硬件的差异,避免一套代码在不同的平台下产生的效果不一致。
在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。
这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
所以,volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。
volatile 提供 happens-before 的保证,同时确保一个线程对共享变量的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到内存,当有其他线程需要读取时,它会去内存中读取新值。
原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
● 对 volatile 变量的写指令后会加入写屏障
● 对 volatile 变量的读指令前会加入读屏障
如何保证可见性
● 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
● 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
如何保证有序性
● 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
● 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile 常用于多线程环境下的单次操作 (单次读或者单次写)。
原子性
volatile不能保证原子性,比如下面这里的读取操作跟写入操作不是原子性的,所以volatile无法保证原子性