参考致谢:
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://blog.youkuaiyun.com/zjcjava/article/details/78406330
http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/
https://blog.youkuaiyun.com/qq_43188744/article/details/108073962
1 volatile前提知识
1.1 内存模型的概念
1、如果一个变量在多个线程中都存在缓存,那么就可能存在缓存不一致的问题。
2、以下2种解决方法:
1)通过在总线加LOCK锁的方式
2)通过缓存一致性协议
缓存一致性协议。最出名的就是Intel 的MESI协议,它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
1.2 java内存模型JMM
见博文:https://blog.youkuaiyun.com/mumu_nuli/article/details/116124639
2 Volatile概述
2.1 Volatile的定义
volatile 是 Java 虚拟机提供的轻量级的同步机制。
2.2 volatile的实现原理
有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令:
- Lock 前缀指令会引起当前处理器缓存写回内存。
- 一个处理器的缓存写回内存会导致其他处理器的的缓存失效 。
2.3 volatile的三大特性
- 保证变量对不同线程的可见性:及时通知其他线程,主内存的变量值已经被修改。
- 禁止指令排序:设置了JSR内存屏障。
- 不保证原子性:JMM保证操作原子性。
2.4 Volatile特性—代码验证
1、代码验证——volatile的可见性
1)普通变量,无法通知其他线程其发生变化。
//线程1
boolean stop = false;//线程1 先读到stop,被线程2抢先运行
while(!stop){
doSomething();
}
//线程2
stop = true;//线程2修改后,再轮到线程1调用时不知道stop已经修改,继续判断就会错误
2)改进:volatile变量后,通知其他线程做了一些操作修改,增强了主内存与各个线程的可见性。
//线程1
boolean volatile stop = false;//加了volatile,线程1得知缓存行失效就要重去主内存读取
while(!stop){
doSomething();
}
//线程2
stop = true;//线程2修改后会报告缓存行无效
2、代码验证——volatile的不保证原子性(Synchronized可保证原子性)
public class Test {
public volatile int inc = 0;
public void increase() {
//这个自增操作不是原子性,分三步读取变量的原始值、加1操作、写入工作内存。
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();//匿名内部类
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
volatile不保证操作的原子性过程:
1、线程1对变量进行自增操作,先读取了变量inc的原始值0,然后线程1被阻塞了;
2、线程2对变量进行自增操作,也读取了变量inc的原始值0(线程1没有对变量进行修改操作)
然后进行加1操作,并把1写入工作内存,最后写入主存。
3、然后线程1接着把之前读到deinc值进行加1操作,再写入工作内存和主内存。
(注意1先读取了变量值,在2之后修改操作,所以volatile修饰的变量只能保证2是从主内存重读,但他读不到1修改的值)
怎么保证操作原子性?
- synchronized同步方法
public volatile int inc = 0;
public synchronized void increase() {
inc++;
}
- Lock给方法加锁
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally{
lock.unlock();
}
}
- AtomicInteger(JUC下 原子性类 AtomicX)
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
3、volatile保证有序性
volatile只能一定程度保证操作有序性。
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
能保证3在12之后45之前执行,并且12的结果对345操作可见。12和45的顺序就不能保证。
3 Volatile应用案例
3.1 DCL单例与volatile问题
/**
* DCL双锁校验:双重检测锁定下的单例模式(线程安全)
* volatile关键字用于防止指令重排序
*/
class Singleton {
//volatile第一次检查单例是否被初始化,然后进入锁的代码块。
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
//再检查一次是否已经初始化单例,可以避免在进过第一次的检查时,检查结果为true,
//但是实际上另一个线程已经准备初始化该单例了.
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
3.2 DCL加volatile的作用
对象的创建过程(new关键字)分为三个阶段:
1)分配对象内存空间
2)初始化对象
3)设置对象指向内存空间
JVM为了加快运行速度会对指令重排序,原本1,2,3的顺序则可能变为1,3,2。此时当代码运行到3时,另一个线程恰好在获取该单例,那么此时代码就会返回一个没有初始化完成的单例对象,这是非常危险的。当使用了volatile修饰这个单例时,就必须先初始化对象,JVM就能确保new一个对象的过程是1,2,3的顺序。
注意:
单例模式中懒汉模式的getInstance在多线程的问题,只用双重检查锁定Double check Lock解决,不加加volatile可以吗?(已经是DCL单例)
--------还会有重排序风险,需要加Volatile,禁止掉重排序!
4 synchronized与volatile
4.1 synchronized的特点
一个线程执行互斥代码过程如下:
获得同步锁;
清空工作内存;
从主内存拷贝对象副本到工作内存;
执行代码(计算或者输出等);
刷新主内存数据;
释放同步锁。
所以,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。
4.2 volatile的特点
volatile是第二种Java多线程同步的手段,volatile变量可以被看作是一种“程度较轻的synchronized”,一个变量被volatile修饰,内存模型确保所有线程可以看到这个变量变化的一致的变量值。
class Test {
//加上volatile将共享变量i和j的改变直接响应到主内存中,保证i和j的值可以一致
static volatile int i = 0, j = 0;
static void one() {
i++;
j++;
}
//不能保证执行two方法的线程是在i和j执行到哪一步获取到的,i若只取没改,下次操作就会用错误的数据。
static void two() {
System.out.println("i=" + i + " j=" + j);
}
}
所以volatile可以保证内存可见性,不能保证并发有序性。
4.3 volatile和synchronized区别
1、volatile本质是在缓存无效时,通知jvm当前变量需要到主存中重读;
synchronized本质是锁定当前变量只有当前线程可以访问,其他线程阻塞。
2、volatile只能使用在变量,synchronized能使用在变量,方法上。
3、volatile只能实现变量修改的可见性,synchronized可以保证变量修改的可见性和原子性。