[JVM系列]别再说不知道volatile了

本文介绍了Java中的volatile关键字,作为轻量级同步机制,它提供可见性和禁止指令重排。但volatile无法保证原子性,文中通过代码示例和JMM(Java内存模型)的规则解析了volatile的工作原理和限制。此外,还探讨了volatile在实现线程安全单例中的应用。
摘要由CSDN通过智能技术生成

闲聊

        做程序员久了,如果你还是写那些crud,那真的应该好好的想一下了。觉得自己如果不了解点别的东西都对不起自己这些年的工作经验,之后出去不是什么10年的工作经验而是一个经验用了10年,不论你出于什么原因学习了jvm,都认为迈出这一步很棒,jvm的介绍我希望用自己的理解表达出来,如果有什么地方说的不好,欢迎大家指正,没准还能认识一下~


一、volatile

        今天小编跟大家分享一下volatile,主要从她的定义,特性,原理,使用上进行分析,希望尽可能以小编认为通俗的方式说出来,当然如果有不对的地方,大家踊跃拍砖~


二、地(ding)位(yi)

        volatile是JVM提供的轻量级同步机制,synchronized也是JVM提供的同步机制,这里为啥说volatile是轻量级呢,正是和synchronized对比得来的,synchronized是重量级的同步机制。volatile不能保证原子性,sync可以保证,至于什么是原子性,就不用我多说了吧。


三、特性

        现在要划重点了,赶紧拿出你的小本本出来,volatile具有的特性是1. 可见性;2禁止指令重排,这个东西就算你不理解,那你也得知道~

3.1、可见性

        可见性说的是,在高并发的情况下,多个线程同时访问一个用volatile修饰的变量,当其中一个线程修改了这个变量的值,其他线程可以立即看到变量的最新值。如果这句话不明白,我们看用代码来说话!

/**
 * 验证volatile的特性
 * Created by Viola on 2019/7/11.
 */
public class MyData {
	int number=0;
	public void addT060(){
        this.number=60;
    }
    public void addTo60(){
        this.number=60;
    }
}
class VolatileDemo{
    public static void main(String[] args) {
       //测试可见性
        seeOkByVolatile();
    }
    //volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
    public static void seeOkByVolatile(){
        MyData myData=new MyData();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in ");
            //暂停一会线程
            try {
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e){
                e.printStackTrace();
            };
            myData.addTo60();
            System.out.println(Thread.currentThread().getName()+"\t update number value->"+myData.number);
        },"childThread:").start();
        //第2个线程时main线程
        while (myData.number==0){
        }
        System.out.println(Thread.currentThread().getName()+"\t mission is over,main get number value is :"+myData.number);
    }
  }

        大家可以猜一下这段代码的运行完,最后一行打印的myData.number是啥,嘻嘻我怎么也开始出题了:

        A、0;
       B、60;
       C、以上都不是;

        如果将MyData 中的 int number=0;改为volatile int number=0;结果又是A、B、C中的哪一个呢?答案先不揭晓了,大家可以先思考一下,如果想要确定答案或是好奇答案,请留言,小编看到会回复~

        上面是表层的,入门级别,也是不够的,人家都不怎么问你啥是可见性了,人家会问你的是volatile可见性是怎么保证的,他的原理是什么?没事别慌,下面原理会说~

3.2、禁止指令重排

        我们写的代码顺序并不是真正执行时候的顺序,这个有疑问的举手~编译成.class的二进制文件,在解释执行时,cpu指令会做一些优化,使得顺序会发生改变。禁止指令重排说的是,我们用volatile修饰的这行代码的前后会分别生成一道不可逾越的屏障,volatile修饰的这样代码的前面的代码不会插进来,之后的代码也不会插进来,当然这行代码也不会跑出去。因此禁止指令重排,保证了代吗执行的有序性。

3.3、能够保证原子性吗?

        NO,不行!经典的例子 i++;i++分三步 1,读取i,2.加1操作,3结果赋值给i。别多说了,show me the code~

/**
 * 验证volatile的特性
 * Created by Viola on 2019/7/11.
 */
public class MyData {
	volatile int number=0;
	//i++,当前是volatile修饰
    public  void addPlusPlus(){
        number++;
    }
 }
class VolatileDemo{
    public static void main(String[] args) {
        //测试原子性
        notAutomic();
    }
    //不能保证原子性
    public static void notAutomic(){
        MyData myData=new MyData();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 1000; i1++) {
                    myData.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }
        //等待上面的10个线程全部执行完成
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finished , number value is \t"+myData.number);
    }
  }

        这段代码的运行结果我们发现,不是9000多就是8000多,循环了1万次,调用了1万次addPlusPlus(),但是结果没有到1万,但是最后的原子性能不能保证依然不用我多说了~


四、原理

       

划重点1   JMM

        JMM(Java 内存模型 java memory model简称JMM)本身是一种抽象的概念,描述了一组规范,规定了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。
        JMM关于同步的规定:

       1. 线程解锁前,必须要把共享变量的值刷回到主内存中
      2. 线程加锁前,必须读取主内存的最新值刷到自己的工作内存
      3. 上面说的加锁和解锁是同一把锁

        由于JVM运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作空间(栈空间),工作空间是每个线程私有的,而java内存模型规定所有变量都存储在主内存中,主内存中的变量是线程共享的,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先将变量从主内存拷贝到工作内存,对变量进行操作,操作完成后写回到主内存中,不能直接操作主内存中的变量,线程间的通信也通过主内存来完成,其简要访问过程如下:
在这里插入图片描述

划重点2   CPU指令重排

        计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排:源代码–>编译器优化的重排–>指令并行的重排–>内存系统的重排–>最终执行的指令。如果两个操作之间的关系不存在下面所列举的先行发生(happen-before)情况,便会出现指令重排:

  1. 程序次序规则:程序执行时按照控制流顺序,要考虑分支和循环。
  2. 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作,这里需要强调2点:第一:同一个锁;第二:后面是时间上的先后。为什么这样呢?前一个线程unlock时可以将最新的变量刷回主内存,后面线程在lock时便会拿到最新的值。
  3. volatile变量规则:对一个volatile变量的写操作优先于后面对这个变量的读操作,原理同2,也是要刷回主内存。
  4. 线程启动规则:Thread对象的start()方法先行发生于次线程的每一个动作。
  5. 线程终止规则:线程中的所有操作先行发生于对此线程的终止检测。
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。(我对这句话实在是不理解,求指教)
  7. 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
  8. 传递性:A操作先行发生于B,B先行与C,那么A先行与C。

        将上面的总结一下可以得出:1.单线程里能够确保程序最终执行结果和代码顺序执行的结果一致。2、处理器在进行重排序时必须要考虑指令之间的数据依赖性


四、使(dan)用(li)

        哪里用我们可以用到volatile呢?其实我们常见的单例,保证线程安全的单例,便是用到了volatile,话不多说了,代码如下:

/**
 * 懒汉模式,单例对象--synchronized
 * 双层同步锁,+volatile 禁止指令重排序是线程安全的
 * 单例实例在第一次使用时创建
 * Created by Viola on 2019/5/8.
 */
@ThreadSafe
public class SingleTonExample5 {
    //私有构造函数
    private SingleTonExample5(){}

    // 1、memory = allocate() 分配对象的内存空间
    // 2、ctorInstance() 初始化对象
    // 3、instance = memory 设置instance指向刚分配的内存

    // JVM和cpu优化,发生了指令重排

    // 1、memory = allocate() 分配对象的内存空间
    // 3、instance = memory 设置instance指向刚分配的内存
    // 2、ctorInstance() 初始化对象

    //私有的单例对象
    private volatile static  SingleTonExample5 instance=null;
    //共有的创建对象的方法
    public static SingleTonExample5 getInstance(){
        if (instance==null){ //A-1
            synchronized (SingleTonExample.class){//双层检测机制
                if (instance==null){
                    instance=new SingleTonExample5(); //A-3
                }
            }
        }
       return instance;
    }
}

       懒汉模式的线程安全的单例,对A-3这行做了详细的说明,如果不用volatile修饰,这时有的线程会拿到一个没有初始化完成的对象而出现错误。


五、结语

        希望您能从小编的文章中得到一些感受或者启发,那将是我最大的荣幸,感谢您宝贵的阅读时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值