说到valatile,那他是什么?能干什么?怎么用呢?让我们来进一步了解。
一、Volatile
是jvm多线程轻量级的同步机制
如果不在多线程的条件下使用volatile那么将无任何意义.
1. Volatile有哪些特性?
1. 保证可见性(某一个线程修改主内存中的值,那么其他线程就会马上得到同通知,volatile不会引起线程上下文切换和调度。)
2. 不保证原子性(不保证在多个线程同时操作同一个变量的时候,不会出现写覆盖,但类似于volatile++这种复合操作不具有原子性,因为本质上volatile++是读、写两次操作)
3. 禁止指令重排(防止汇编源码重新排列)
2.上面说的什么我怎么有点糊涂?
不要急下面我们慢慢来讲解。
说到上面的3个特性,那我们得从JMM说起?
2.1什么是JMM?
Jmm(JAVA 内存模型JAVA Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段,构成数组对象的元素)的访问方式.
2.2jvm和jmm之间的关系
jmm中的主内存、工作内存与jvm中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存
2.3JMM有哪些特性?
- 可见性(某一个线程修改主内存中的值,那么其他线程就会马上得到同通知)
- 原子性
- 有序性
JMM关于同步的规定:
1. 线程解锁前,必须把共享变量的值刷新回内存.
2. 线程加锁前,必须读取内存的最新值到自己的工作区
3. 加锁,解锁是同一把锁.
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈内存空间),工作内存是每一个线程的私有数据区域,而java内存模型中规定所有变量都存储在<font color=#990033 size=4 face="黑体">主内存</font>,主内存是共享变量的区域,所有线程都可以访问,<font color=#990033 size=4 face="黑体">但线程对变量的操作(读取赋值等)必须在内存中进行,首先要将变量从主内存拷贝到自己的内存空间,然后对变量进行操作,操作完之后将变量写回主内存,不能直接操作主内存的变量,各个线程中的工作内存存储着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,线程间的通讯(传值)必须通过主内存来完成</font>,其简要访问如下图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200227203626738.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNTM5NDM3,size_16,color_FFFFFF,t_70)
**根据这个图我们在来看看Volatile的可见性:**
```java
public class Count_demo {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 1000; i1++) {
count = count + 1;
}
}
},String.valueOf(i)).start();
}
Thread.sleep(1000);
System.out.println("count 的和为:"+count);
}
}
上述代码我们 没有加 Volatile修饰 看看 结果
显然 结果 为 18272,那么 这是 为什么 呢 ,其实 就是 每个内存 都在 操作自己的内存,
并没有把自己内存空间里的值写回主内存,而是更改堆中地址对应的值.
那么我们将进一步认证:
public class Count_demo {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 1000; i1++) {
count = count + 1;
}
System.out.println(Thread.currentThread().getName()+"值为:" + count);
}
},String.valueOf(i)).start();
}
Thread.sleep(1000);
System.out.println("count 的和为:"+count);
}
}
结果 :
简单明了 每个 线程都是在 操作属于自己的那块空间,并没有把值写会主内存,修改堆中的地址对应的值
那么现在让我们加volatile:
public class Count_demo {
public static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 1000; i1++) {
count = count + 1;
}
System.out.println(Thread.currentThread().getName()+"值为:" + count);
}
},String.valueOf(i)).start();
}
Thread.sleep(1000);
System.out.println("count 的和为:"+count);
}
}
结果:
很显然出现了重复写的动作;
那我我们 就来论述一下 这个 重复写的过程:
打个比方就是在程序语言里java会把.java文件编译成.class文件,再把class文件编译成一种汇编语言,在这个汇编语言中每一行会有自己的标识,即使其他线程抢到cpu,当此线程在执行的时候会在汇编码里找到对应的行继续执行.
3.使用场景-单例模式
package com.cs.volatileT;
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton _instance = null;
public static DoubleCheckSingleton getInstance() {
if (_instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (_instance == null) {
_instance = new DoubleCheckSingleton();
}
}
}
return _instance;
}
}
在java中一条语句的赋值,在字节码下就会出现多条(汇编码就更多了):
new #3 <concur/DoubleCheckSingleton>
invokespecial #4 <concur/DoubleCheckSingleton.<init>>
putstatic #2 <concur/DoubleCheckSingleton._instance>
4.volatile变量读
package com.one;
import java.util.concurrent.ThreadFactory;
public class Test {
public static boolean a = false;
public static void set(){
System.out.println("第一次修改a为true");
a = true;
System.out.println("第一次修改完毕 记为A");
System.out.println("第二次修改a为false");
a = false;
System.out.println("第二次修改完毕 记为B");
System.out.println("第三次修改a为true");
a = true;
System.out.println("第三次修改完毕 记为A");
}
static Object object = new Object();
static boolean b = false;
public static void get0(){
System.out.println("首次获取a:" + a);
int i = 0;
int a1 = 0;
while (i < 100000){
if (b != a){
b = a;
a1 ++;
System.out.println("第"+ a1 +"次获取修改得值:" + a);
}
i++;
}
System.out.println(a1);
}
public static void main(String[] args) {
new Thread(() ->{
get0();
}).start();
new Thread(() ->{
set();
}).start();
}
}
结果一:正常
结果二:正常
happens-before
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,B happens-before C,那么 A happens-before C。
as-if-serial语义
无论怎么重排序,单线程的执行结果不会改变.
上面两种结果均正确是因为,锁的happens-before规则保证锁释放和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入.
5、内存屏障(扩展-不太好记)
volatile重排序规则表
是否能重排序 | 第二个操作 | 第二个操作 | 第二个操作 |
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | YES | YES | NO |
volatile读 | NO | NO | NO |
volatile写 | YES | NO | NO |
- LoadLoad
在每个volatile写操作后面添加LoadLoad屏障
禁止上面的volatile读和下面的volatile读重排序 - LoadStore
在每个volatile读操作后面添加LoadStore屏障
禁止下面的普通写和上面volatile读重排序 - StoreLoad
在每个volatile写操作后面面添加StoreLoad屏障
防止上面的volatile写与后面可能有的volatile读/写重排序 - StoreStore
在每个volatile写操作前面添加StoreStore屏障
禁止上面的volatile写与下面的volatile写重排序
在x86操作系统除StoreLoad其他屏障都可省略掉,在x86处理器下,仅会对写-读操作重排序。X86不会对读-读、读写和写写操作重排序,因此在X86处理器下会省略掉其他3种屏障,在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读内存的语意。这意味这在X86处理器中volatile写的开销比voaltile读的开销会大的多,因为执行StoreLoad屏障开销比较大.
6、CPU扩展
注:CPU术语
术语 | 英文单词 | 术语描述 |
---|---|---|
内存屏障 | memory barriers | 是一组处理器的指令,用于实现对内存操作的顺序限制 |
缓冲行 | cache line | 缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行,需要使用多个主内存读周期 |
原子操作 | atomic operations | 不可中断的一个或一系列操作 |
缓存行系统 | cache line fill | 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1,L2,L3的或所有) |
缓存命中 | cache hit | 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取 |
写命中 | write hit | 当处理器将操作数写回到一个内存缓存区域时,它首先会先检查这个缓存的内存地址是否在缓冲行中,如果存在一个有效的缓存行,则处理器将这个操作数写回缓存行,而不是写回内存,这个操作被称为写命中 |
写缺失 | write misses the cache | 一个有效的缓存行被写入到不存在的内存区域 |
注:Lock前缀指令在多核处理器下
1)将当前处理器的缓存行的数据写回到系统内存.
2)这个写回内存的操作会使在其他CPU里缓存了改内存地址的数据无效.
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存.
如果是多核处理器,那么其他处理器会嗅探在总线上的值来检查自己缓存的值是不是过期了,当处理器发现自己的缓存行对应的内存
地址被修改,就会把当前的缓存行视为无效状态,会重新从系统内存中把数据读到处理器的缓存里.
volatile两条实现原则
1)Lock前缀指令会引起处理器缓存写回到内存
声言处理器的Lock#信号,在多处理器环境中LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。但是在最近的处理
器里,LOCK#信号一般不锁总线,而锁缓存,锁总线的开销大。在锁操作时总是在总线上声言LOCK#信号,如果访问的内存区域已经
缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区域的缓存并写回内存,并使用缓存一致性机制来确保修改的
原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据.
2)一个处理器的缓存写回到内存会导致其他处理器的缓存无效.
IA-32处理器和Intel 64处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器
系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和他们的内部缓存。处理器使用嗅探技术保证它的内部
缓存、系统内存和其他处理器的缓存的数据在总线上保持一致.
MESI协议缓存状态
可以查看相关连接【https://www.cnblogs.com/jokerjason/p/9584402.html】
状态 | 描述 | 监听任务 |
---|---|---|
M 修改 (Modified) | 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 | 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。 |
E 独享、互斥 (Exclusive) | 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 | 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。 |
S 共享 (Shared) | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。 |
I 无效 (Invalid) | 该Cache line无效。 | 无 |