1 volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后 那么就具备了两层语义:
1)保证了不同线程对共享变量进行操作时的可见性
即一个线程修改了共享变量的值,那么修改后的值对其他线程来说是立即可见的
2)禁止进行指令重排序
public class TestVolatile extends Thread{
private boolean flag = false; //注意这里
//private volatile boolean flag = false; 若改成这个就没问题了
public void setStop(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "==执行==");
while (true) {
if (flag) {
System.out.println(name + "==退出==");
break;
}
}
System.out.println(name + "==线程结束==");
}
public static void main(String[] args) throws Exception {
TestVolatile tv = new TestVolatile();
tv.start();
Thread.sleep(3000);
tv.setStop(true);
//在主线程方法里修改了flag的值
//但是另一个线程还一直在死循环中 看不到主线程这里修改的值
System.out.println("main finish");
}
}
不加volatile的运行结果:
![7FZ]KEJOIC(SPYJX4%EFMB.png
tv线程在运行的时 会flag变量的值拷贝一份放在自己的工作内存当中
当主线程线程更改了flag变量的值之后 但是还没来得及写入主存当中
主线程转去做其他事情了 那么tv线程由于不知道主线程对flag变量的更改 因此还会一直循环下去
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存
第二:使用volatile关键字的话 当线程A进行修改时 会导致线程B的工作内存中缓存变量的缓存行无效
(反映到硬件层的话 就是CPU的L1或者L2缓存中对应的缓存行无效)
比如这里 主线程修改了flag的值 会导致tv线程的工作内存中缓存变量的缓存行无效
所以tv线程再次读取变量flag的值时会去主存读取
那么tv线程读取到的就是最新的正确的值
2 volatile保证原子性吗?
volatile无法保证对变量的操作的原子性
比如自增操作是不具备原子性的,它包括从主存中读取值复制到工作内存中、在工作内存中对变量副本进行加1操作、写回主存
那么就是说自增操作的三个子操作可能会分割开执行 就有可能导致下面这种情况出现:
假如某个时刻变量 i 的值为1
1)线程A对变量进行自增操作 线程A读取了变量的值 然后此时线程A被阻塞了
2)然后线程B对变量也进行自增操作
由于线程A只是对变量 i 进行读取操作 而没有对变量进行修改操作 所以不会导致线程B的工作内存中缓存变量 i 的缓存行无效
那么线程B会直接去主存读取 i 的值放入线程内存中 i 的值为1
然后进行加1操作 然后将值写入主存
3)线程B将2写回主存 不会把线程A的缓存行设为无效吗?
只有在做读取操作时 发现自己缓存行无效 才会去读主存的值
而线程A的读取操作在线程B写入之前已经做过了 所以这里线程A可以继续做自增了
所以线程A对 i 进行加1操作后 i 的值为2 最后将值写回主存
如上两个线程分别进行了一次自增操作后 i 只增加了1
所以若想保证原子性 可以:
1)采用synchronized
2)采用Lock
3)采用AtomicInteger
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类 即对基本数据类型的 自增(加1操作)、自减(减1操作)、以及加法操作(加一个数)、减法操作(减一个数)进行了封装 保证这些操作是原子性操作
atomic是利用CAS来实现原子性操作的(Compare And Swap)CAS实际上是利用处理器提供的CMPXCHG指令实现的 而处理器执行CMPXCHG指令是一个原子性操作
3 volatile能保证有序性吗?
volatile关键字能禁止指令重排序 所以volatile能在一定程度上保证有序性
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时 在其前面的操作的更改肯定全部已经进行
且结果已经对后面的操作可见 在其后面的操作肯定还没有进行
2)在进行指令优化时 不能将在对volatile变量访问的语句放在其后面执行
也不能把volatile变量后面的语句放到其前面执行
举个简单的例子:
x = 2; //语句1
y = 0; //语句2
volatile flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量 那么在进行指令重排序的过程的时候 不会将语句3放到语句1、语句2前面
也不会讲语句3放到语句4、语句5后面
但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的
并且volatile关键字能保证 执行到语句3时 语句1和语句2必定是执行完毕了的
且语句1和语句2的执行结果对语句3、语句4、语句5是可见的
指令重排在单例模式中的影响 基于双重检验的单例模式(懒汉型)
public class Singleton {
private static Singleton instance; //注意这里
//private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
//先判断是否为空 不为空先加锁
synchronized(Singleton.class) {
//加锁时有可能别的线程已经创建好对象了 所以加锁完后还需要再判断一次是否为空
if (instance == null)
instance = new Singleton();// 非原子操作
}
}
return instance;
}
}
instance= new Singleton()并不是一个原子操作 其实际上可以抽象为下面几条JVM指令:
1)分配内存空间
2)初始化对象(需要用到1中分配的空间)
3)设置instance指向分配的内存地址
所以上述三条指令也可重排序为:
1)分配内存空间
2)设置instance指向分配的内存地址
3)初始化对象(需要用到1中分配的空间)
为什么这里用了synchronized还要用volatile?
具体来说就是synchronized虽然保证了原子性 但却没保证指令重排序的正确性 且**程序可能是在多核CPU上执行**
比如线程A执行的是重排序后的指令 所以A线程的instance还没有造出来 但已经被赋值了(即先分配内存空间后instance指向分配的内存地址)
而B线程这时过来了(发现instance不为null)错以为instance已经被实例化出来,一用才发现instance尚未被初始化,这就造成了空指针
volatile底层原理
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现 加入volatile关键字时 会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏)内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置 也不会把前面的指令排到内存屏障的后面
即在执行到内存屏障这句指令时 在它前面的操作已经全部完成
2)它会强制将对缓存的修改操作立即写入主存
3)如果是写操作 它会导致其他CPU中对应的缓存行无效