JUC并发编程(10)--- 谈谈对Volatile的理解

Posted 小样5411

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程(10)--- 谈谈对Volatile的理解相关的知识,希望对你有一定的参考价值。

前言

什么是Volatile?
答:Volatile是Java虚拟机提供的轻量级同步机制

Volatile的三大特性:
1、保证可见性
2、不保证原子性
3、禁止指令重排

一、保证可见性

讲可见性就不得不提JMM,我们先来了解什么是JMM?
JMM:java内存模型,就是一个约定、规范,实际不存在。

关于JMM的一些同步约定
1、线程加锁前,必须读取主内存中的最新值到工作内存中
2、线程解锁前,必须把共享变量立刻刷回主存
3、加锁和解锁是同一把锁

我通过线程运行机制了解它
在这里插入图片描述

图中8种操作即线程运行的8种操作
分析:如图,首先加锁前要将主存最新值读取到工作内存,通过read和load操作,读取到工作内存就需要处理执行,use和assign(执行和返回),解锁前通过write和store操作刷回主存。过程就是read和load,lock,use和assign,write和store,unlock,一共8个操作,其中红色框框出的操作都是成对执行的

存在问题:线程A和线程B同时执行时,线程B将主存中Flag=true刷新成了Flag=false,但线程A不能及时可见,还用的是true,就是不知道线程B将Flag改变了,于是就要用到Volatile,保证可见性,让线程A看得到除自己以外的线程B操作,进而知道Flag被改变了

我们可以看下面代码再具体理解

package com.yx.JMM;

import java.util.concurrent.TimeUnit;

public class JMMDemo {
    private  static boolean Flag = true;
    public static void main(String[] args) throws InterruptedException {


        new Thread(()->{//线程A
            while (Flag==true){

            }
        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{//线程B
            change();
        },"B").start();

    }
    public static void change(){
        Flag=false;
        System.out.println(Flag);
    }
}

在这里插入图片描述

线程A先执行,进入循环,然后1秒后线程B执行调用change方法,打印改变后的Flag,即false,但是程序一直不停止,陷入死循环。因为线程B改变了Flag,线程A不可见,所以依然认为Flag=true,一直在循环。

我们加一个volatile修饰,再运行程序,就不会出现死循环了,正常结束
在这里插入图片描述

在这里插入图片描述

这就是volatile保证可见性,线程B改变,线程A也可见。不然还可能因为线程B中途修改了主存中的值,但是线程A不知道,处理完又把自己的变量写回主存,造成和修改之前一样,相当于线程B没有修改。比如初始Flag=true,然后线程A读取后,线程B再来将其修改为false,但线程A不知道,最后又把Flag变为true,这就相当于没变了。

二、不保证原子性

什么是原子性?
原子性就是不可分割,如线程A在执行时,不能被打扰,也不能被分割

我们来看一个案例

package com.yx.JMM;

public class vDemo {
    //volatile不保证原子性
    private volatile static int num = 0;

    public static void add(){
        num++;
    }

    public static void main(String[] args) {
        //理论应该是2w
        //执行20个线程,每个都加1000
        for (int i = 0 ; i < 20 ; i++){
            new Thread(()->{
                for (int j = 0 ; j < 1000 ; j++){
                    add();
                }
            }).start();
        }
        while (Thread.activeCount()>2){//至少有main和GC线程
            Thread.yield();//线程让步
        }
        System.out.println(num);
    }
}

理论上有20000,但怎么执行都到不了2w
在这里插入图片描述

为什么会这样呢
因为add()中的num++就不是原子操作,会被多个线程占用从而不自增
我们来将它的字节码,变成汇编形式

在这里插入图片描述

可以看到,一个num++在汇编下,有多行命令,
getstatic表示获取原始值,iadd表示+1,putstatic表示将结果写回,
也就是说它本身在JVM中并不是原子性的操作,多线程情况下从主存中取值(write和load),
在线程工作内存运算,写回主存的时候可能会出现覆写情况,所以最终结果无法到20000

如何保证原子性?
synchronized和lock肯定能保证原子性,但比较耗费资源,有没有更优的方法
在这里插入图片描述

在这里插入图片描述
如果不加synchronized和lock怎么保证原子性?

在这里插入图片描述
用上面这个atomic类就可以,用原子类的int,就是AtomicInteger,更安全,高效

package com.yx.JMM;

import java.util.concurrent.atomic.AtomicInteger;

public class vDemo {
    //volatile不保证原子性
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void add(){
        num.getAndIncrement();//AtomicInteger+1,用的是CAS:cpu的并发原语,效率极高
    }

    public static void main(String[] args) {
        //理论应该是2w
        //执行20个线程,每个都加1000
        for (int i = 0 ; i < 20 ; i++){
            new Thread(()->{
                for (int j = 0 ; j < 1000 ; j++){
                    add();
                }
            }).start();
        }
        while (Thread.activeCount()>2){//至少有main和GC线程
            Thread.yield();//线程让步
        }
        System.out.println(num);
    }
}

正常结果20000
在这里插入图片描述
num.getAndIncrement()十分高效,并且有别于num++,它是在底层,在内存中修改值,这涉及CAS,后面专门发文讲。

三、禁止指令重排

指令重排:计算机并不是按你写的程序那样执行的,处理器会考虑指令之间的依赖性来重排

源代码->编译器优化重排->指令并行也会重排->内存系统也会重排->执行

在这里插入图片描述
如上图,指令重排,可能会导致指令执行顺序不一致,导致赋值或者逻辑不一致,从而结果异常。虽然指令重排造成错误情况出现几率极少,1000万次重排,都不会出现一次,但理论上可能出现,我们需要了解下这个。

volatile如何避免指令重排?
有内存屏障,它就是一个CPU指令,可以保证特定操作的执行顺序,因为它可以禁止指令间的顺序颠倒,如图,只要加了volatile就会有内存屏障,禁止执行顺序交换、重排
在这里插入图片描述
总结:volatile可以保证可见性,不能保证原子性,避免出现指令重排现象

视频讲解(狂神):https://www.bilibili.com/video/BV1B7411L7tE?p=30

以上是关于JUC并发编程(10)--- 谈谈对Volatile的理解的主要内容,如果未能解决你的问题,请参考以下文章

你真的懂并发吗?谈谈对JUC线程池ThreadPoolExecutor的认识吧

Juc_并发编程目录

Juc_并发编程目录

2.6W + 字,彻底搞懂 JUC!

JUC并发编程 共享模式之工具 JUC Semaphore(信号量)-- 限制对共享资源的使用 改进数据库连接池

Java并发编程 JUC中的锁