JMM理论三:volatile详述

参考致谢:
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://blog.csdn.net/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.csdn.net/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.csdn.net/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
能保证312之后45之前执行,并且12的结果对345操作可见。1245的顺序就不能保证。

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可以保证变量修改的可见性和原子性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值