当前位置: 首页 > 科技观察

Volatile关键字是否保证原子性?

时间:2023-03-13 22:12:20 科技观察

说到volatile这个关键字,阿芬觉得看过阿芬文章的人一定对这个关键字非常熟悉,因为如果你是做Java开发的,如果面试涉及到多线程,那么很多面试官会询问volatile关键字的使用及其作用。今天阿粉就来说说volatile的关键作用以及它的一些特性。volatilevolatile是Java中比较重要的关键字,主要用来修饰不同线程会访问和修改的变量。而这个变量只能保证两个特性,一个是保证顺序,一个是保证可见性。那么什么是秩序,什么是可见性?秩序那么什么是秩序?实际上,程序执行的顺序是按照代码的先后顺序执行的,禁止指令重排序。这似乎是理所当然的,但事实并非如此。指令重排序是JVM在不影响单线程程序执行结果的情况下,对指令进行优化,提高程序运行效率,并尽可能增加并行度。但是在多线程环境下,部分代码的顺序发生了变化,可能会导致逻辑错误。而volatile正是因为这个特性而广为人知。volatile保证顺序如何?网上很多小伙伴都说volatile可以禁止指令重排序,保证代码程序会严格按照代码的顺序执行。这样可以保证秩序。对volatile修饰的变量的操作会严格按照代码顺序执行,也就是说当代码执行到volatile修饰的变量时,必须执行前面的代码,不能执行后面的代码.如果此时面试官没有继续深挖,那么恭喜你,你可能已经回答了这个问题,但是如果面试官继续深挖,为什么会禁止命令重排呢?什么是命令重排?从源码到指令执行,一般有3种重排,如图:接下来我们要看看volatile是如何禁止指令重排的。我们直接用代码来验证。公共类ReSortDemo{inta=0;布尔标志=假;publicvoidmehtod1(){a=1;标志=真;}publicvoidmethod2(){if(flag){a=a+1;系统输出。println("最后一个值:"+a);}}}如果有人看到这段代码,肯定会说,这段代码会是什么结果呢?有人说是2,是的,如果只是用单线程调用,结果就是2,但是如果用多线程调用,最后输出的结果不一定是我们想象的2,那么我们需要把两个变量都设置为volatile。如果你对单例模式比较了解,你一定关注过这个volatile,为什么呢?我们来看看下面的代码:classSingleton{//不是原子操作//privatestaticSingletoninstance;//改进,Volatile可以保持可见性,但是不能保证原子性。因为有了内存屏障,就可??以保证避免指令重排现象的产生!privatestaticvolatileSingleton实例;//构造函数私有化privateSingleton(){}//提供一个静态public方法,加入双重校验代码,解决线程安全问题,同时解决懒加载问题,保证效率,推荐publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}返回实例;}}上面的单例模式你熟悉吗?是的,这就是**DoubleCheck(DCLLazyStyle)**有人会说,因为指令重排序的存在,双端检索机制不一定是线程安全的,是的,所以阿凡使用了synchronized关键字使其成为线程安全的。可见性其实可见性就是,在多线程环境下,共享变量的修改是否立即对其他线程可见。那么他的知名度一般体现在哪里呢?它在哪里使用?其实在阿芬的认知中,这个变量一般是用来保证可见性的。比如定义了一个全局变量,其中有一个循环判断这个变量的值,一个线程修改这个参数,循环将停止并跳转到稍后执行。下面看一下没有进行volatile修改的代码实现:publicclassTest{privatestaticbooleanflag=false;publicstaticvoidmain(String[]args)throwsException{newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("线程A开始执行:");for(;;){if(flag){System.out.println("跳出循环");break;}}}}).start();线程.睡眠(100);newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("线程B开始执行");flag=true;System.out.println("flag已更改");}})。开始();}}结果必须想象。运行的结果一定是:线程A开始执行:线程B开始执行,标志已经改变。就是这样。如果我们使用volatile,那么这段代码的执行结果会不一样吗?让我们试试:publicclassTest{privatestaticvolatilebooleanflag=false;publicstaticvoidmain(String[]args)throwsException{newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("线程A开始执行:");for(;;){if(flag){System.out.println("跳出循环");break;}}}}).start();线程.sleep(100);newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("线程B开始执行");flag=true;System.out.println("Theflaghaschanged");}})。开始();}这样,我们又可以看到一个执行结果,循环中的输出语句就可以执行了。也就是说,在线程B中,我们对修改后的变量进行修改,最终在线程A中,就可以成功读取到我们的数据信息了。能不能保证原子性不行,我们看一些代码,volatile修饰的变量;publicclassTest{//volatile不保证原子性//原子性:保证数据的一致性和完整性volatileintnumber=0;publicvoidaddPlusPlus(){number++;}publicstaticvoidmain(String[]args){测试volatileAtomDemo=newTest();for(intj=0;j<20;j++){newThread(()->{for(inti=0;i<1000;i++){volatileAtomDemo.addPlusPlus();}},String.valueOf(j))。开始();}//后台默认有两个线程:一个是主线程,一个是gc线程while(Thread.activeCount()>2){Thread.yield();}//如果volatile保证原子性,最后的结果应该是20000//但每次程序执行的结果都不等于20000System.out.println(Thread.currentThread().getName()+"最终数字结果="+volatileAtomDemo.number);}}如果能保持原子性,最后的结果应该是20000,但是每次的最终结果并不能保证都是20000,例如:mainfinalnumberresult=17114mainfinalnumberresult=20000mainfinalnumberresult=19317三次执行,结果都不同为什么会这样?这与number++有关。number++分为3条指令:执行GETFIELD以获取主存中的原始值number。执行IADD加1,执行PUTFIELD将工作内存中的值写回主内存。当多个线程并发执行PUTFIELD指令时,会出现回写到主存覆盖的问题,所以最终结果不会是20000,所以volatile不能保证原子性。那么,你知道怎么回答吗?