volatile关键字之最通俗易懂的讲解(可见性、不保证原子性、禁止指令重排)

volatile是什么?对volatile的理解?

volatile是java虚拟机提供的轻量级的同步机制,有三大特性:

1. 保证可见性
2. 不保证原子性
3. 禁止指令重排

JMM内存模型之可见性

在这里插入图片描述

在这里插入图片描述
验证volatile的可见性代码:
对Data类的number不添加volatile关键字,执行以下代码

package VolatileDemo;

import java.util.concurrent.TimeUnit;

class Data{
    int number=0;
    /*设置一个修改number的方法*/
    public void updateNumner(int n){
        this.number=n;
    }
    }
public class vilatileDemo {
    public static void main(String[] args) {
        Data data= new Data();/*此时主线程new一个data对象* data.number=0*/
        /*此时开启一个A线程更改number的值*/
        new Thread(()->{
            /*①*/
            System.out.println(Thread.currentThread().getName()+"==>come in");
            /*此时让该线程sleep两秒,为了再更改number之前主线程继续向下执行*/
            try {
                TimeUnit.SECONDS.sleep(1);
               /*③*/ data.updateNumner(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"==>update number= "+data.number);

        },"A").start();

        while(data.number==0){
            /*②A线程休眠两秒,主线程运行到这会在这里循环,A休眠结束继续向下执行*/
        }
        System.out.println(Thread.currentThread().getName()+"task over");
    }
}

运行结果:(注意任务还没停止)
在这里插入图片描述上述结果解析:
1)首先 main线程开始执行,创建 一个 data对象至内存中,main线程拷贝data至main线程缓存
2)主线程main 新建子线程 A,线程A从内存中拷贝data,线程A也开始执行,接着线程A休眠1秒
3)在子线程A休眠1秒的同时主线程执行到while语句,此时主线程data.number等于0,main线程进入无线循环
4)子线程A休眠结束,修改data.number=10
5) 子线程A修改data.number=10,但是只是修改了子线程A缓存的data对象,修改后并不会写回到内存中,又由于两个线程之间的隔离,main线程无法得知子线程改动自己缓存的data.number,主线程则一直无线循环下去。

加了volatile关键字之后运行结果:

 volatile int number=0;

在这里插入图片描述
两种运行结果分析:
一、未加volatile关键字
开始,主线程和A线程都从主内存中复制一份副本data.number到自己的工作内存当中,此时number都等于0,因为A线程休眠秒,保证了主线程进入while循环,此时主线程的工作内存中的number依旧等于0,A线程休眠结束之后对number进行了一次更新为number=10的操作,此时A的工作内存number=10,并且将number=10写会主内存,此时主内存number=0,但是主线程并不知道A线程更改了主内存的number,更没法从A线程获取number已经修改,主线程的工作内存number依然等于10,while循环条件未破坏,无法继续向下执行!
二、加了volatile关键字
加了volatile关键字后,只要主内存的number值发生了改变,main线程(其他线程)就会接收到通知,从而更新自己工作内存中的number(这就体现了volatile保证了可见性),while循环结束,主线程继续向下执行!

在这里插入图片描述

volatile不保证原子性

原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可被加塞或者分割。需要整体完整,要么同时成功,要么同时失败。

volatile不保证原子性案例:

package VolatileDemo;

import java.util.concurrent.TimeUnit;

class Data{
    volatile int number=0;
    /*设置一个number++的方法*/
    public void numberadd(){
        number++;
    }
    }
public class vilatileDemo {
    public static void main(String[] args) {
      
        Data data= new Data();
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    data.numberadd();
                }
            },String.valueOf(i)).start();
        }
        /*等待上面的二十个线程执行完毕,main线程再继续向下执行,Java虚拟机默认存在两个线程:main+GC*/
        while(Thread.activeCount()>2){
            Thread.yield();/*主线程礼让*/
        }
        /*获取最终计算完的number结果*/
        System.out.println("finally number answer :"+data.number);

    }
}

运行结果:
在这里插入图片描述
结果分析:
如果volatile保证原子性,理论情况下,number的最终值应该为20000,但是运行结果却未达到20000这个数,这就说明了volatile并不保证原子性。

volatile为什么不保证原子性呢
举例说明:
在这里插入图片描述
假设有两个线程同时要对num进行num++操作,理想的情况下是两个线程从主内存中复制num=0到工作内存当中,假设线程A先执行num++,线程A的工作内存中num=1,接着写回主内存,主内存的num=1,接着通知B线程的工作内存更改成num=1,然后线程B进行num++,此时B线程的工作内存num=2,在写回主内存num=2,通知A线程主内存num值发生改变。但是这是在多线程的情况下进行操作的,多线程情况下存在线程执行权存在抢占的情况,上述过程中,当A线程将num=1写回主内存前,线程执行权被B线程获取,线程B不知道线程A的工作内存num值发生改变(因为线程A还没把num=1写回主内存并通知B线程),B线程执行num++并写回主内存,此时A线程也写回num=1。这种情况就会出现num++两次但是num还是等于1!

如何解决不保证原子性的问题呢?
方式一:
使用synchronized关键字

package VolatileDemo;

import java.util.concurrent.TimeUnit;

class Data{
    volatile int number=0;
    /*设置一个number++的方法*/
    public synchronized void numberadd(){
        number++;
    }
    }
public class vilatileDemo {
    public static void main(String[] args) {

        Data data= new Data();
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    data.numberadd();
                }
            },String.valueOf(i)).start();
        }
        /*等待上面的二十个线程执行完毕,main线程再继续向下执行,Java虚拟机默认存在两个线程:main+GC*/
        while(Thread.activeCount()>2){
            Thread.yield();/*主线程礼让*/
        }
        /*获取最终计算完的number结果*/
        System.out.println("finally number answer :"+data.number);

    }
}

执行结果:
在这里插入图片描述
这是使用的就是悲观锁的概念(操作数据方法加锁,使用该方法前都要先获取锁才能执行)

java.util.concurrent.atomic包下的类 来保证操作的原子性

package VolatileDemo;
import java.util.concurrent.atomic.AtomicInteger;

class Data{
    AtomicInteger atomicInteger= new AtomicInteger();
    public  void atomicIntegeradd(){
        atomicInteger.getAndIncrement();
    }
}
public class vilatileDemo {
    public static void main(String[] args) {
        Data data= new Data();
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    data.atomicIntegeradd();
                }
            },String.valueOf(i)).start();
        }
        /*等待上面的二十个线程执行完毕,main线程再继续向下执行,Java虚拟机默认存在两个线程:main+GC*/
        while(Thread.activeCount()>2){
            Thread.yield();/*主线程礼让*/
        }
        /*获取最终计算完的number结果*/
        System.out.println("finally number answer :"+data.atomicInteger);

    }
}

执行结果:
在这里插入图片描述
这种方式就是使用了乐观锁!(至于为什么后面再详解)

volatile禁止指令重排

在这里插入图片描述
未禁止指令重排可能导致不同线程数据不一致性案例:
在这里插入图片描述
可能出现情况解析如下:
在这里插入图片描述
第一种情况:线程A先开始执行方法一,一次执行语句一a=1,a=1写回主内存,语句二flag=true,flag=true协会主内存,线程B接着执行方法2,此时主内存中a=1,flag=true,发出就会输出a的值为6
第一种情况:线程A依旧先开始执行方法一,但是存在指令重排,语句一二不存在数据依赖,这次可能线执行语句二flag=true,flag=true写回主内存,此时主内存flag=true,a=0,但是在多线程的情况下线程执行存在抢占情况,此时线程B抢占到线程执行权,接着执行就会输出0+5=5的情况。
出现以上两种结果截然不同的是指令重排导致多线程下数据不一致的发生执行结果不一致!

解决这种情况有两种方式
给方法1方法2加synchronized关键字(效率低,多线程情况下只能有一个线程执行一个方法)
volatile关键字(效率高,多线程情况下多个线程可以操作一个方法)

volatile究竟如何实现指令重排的呢?

volatile实现禁止指令重排优化,从而避免多线程下程序出现乱序执行的现象

先来了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序。
二是保证某些变量的内存可见性。(利用该特性实现的volatile内存可见性)
由于编译器核处理器都内执行指令重拍优化。如果指令间插入一条内存屏障则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier 指令重排序,也就是说通过内存屏障禁止在内存屏障前后的指令顺序进行重排序优化。内存屏障另一个作用就是强制是算出各种CPU的缓存数据,因此任何CPU上的线程都能督导这些数据的最新版本。(图解如下:)
在这里插入图片描述
volatile关键字在哪里经常使用?
单例模式(DCL双端检索)
为什么?下面先简单说明 ,这个之后会进行更新讲解,欢迎大家关注我的博客,会持续更新JUC内容!
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值