[Java] volatile 详详解![通俗易懂]

[Java] volatile 详详解![通俗易懂]前言:要真正搞懂volatile的特性需要与JMM对比来看JMM(线程安全的保证)JMM:JAVA内存模型(javamemorymodel)是一种抽象概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。JMM关于同步的规定线程解锁前,必须把共享变量的值刷新回主内存;线程加锁前,必须读取主内存的最…

大家好,又见面了,我是你们的朋友全栈君。

前言

要真正搞懂volatile的特性需要与JMM对比来看

JMM(线程安全的保证)

JMM:JAVA内存模型(java memory model) 是一种抽象概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定

  • 线程解锁前,必须把共享变量的值刷新回主内存;
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存;
  • 枷锁解锁是同一把锁。

由于JVM运行程序的实体是线程。而每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域。
JMM中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读写)必须在工作内存中进行。具体步骤:首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存存储着主内存中的变量副本拷贝,因此不同线程间无法访问对方的工作内存,线程的通信必须通过主内存来完成

如图所示 可以说cache缓存 就是 这种JMM内存模型的硬件抽象
在这里插入图片描述
JMM的特性:可见性,原子性,有序性。

说回到volatile

volatile

volatile 是java虚拟机提供的轻量级同步机制
导致并发问题的源头是 : 多核 CPU 缓存导致程序的可见性问题、多线程间切换带来的原子性问题以及编译优化带来的顺序性问题。

下面是三个volatile特性

可见性

用代码证明volatile的可见性

class MyDate { 
   
    //共享变量 1.1 首先不加volatile关键字
    int number = 0;

    public void change() { 
   
        this.number = 60;
    }
}

/** * 1.验证volatile的可见性 */
public class VolatileDemo { 
   
    public static void main(String[] args) { 
   
        MyDate myDate = new MyDate();
        new Thread(() -> { 
   
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try { 
   
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) { 
   
                e.printStackTrace();
            }
            myDate.change();
            System.out.println(Thread.currentThread().getName() + "\t update number value=" + myDate.number);
        }, "A线程").start();

        //main线程读到的数据
        while (myDate.number == 0) { 
   
            //main 线程一直循环等待直到number值不等于0
        }
        System.out.println(Thread.currentThread().getName() + "\t over number value="+myDate.number);
    }
}

首先new了一个名字叫A线程的线程。先让线程sleep三秒钟,此时A线程和main线程都拿到了number的值。然后A线程修改了number的值为60。但是main线程因为没有可见性不知道值发生了修改。还是继续循环等待,证明了此时线程之间没有可见性。
在这里插入图片描述
1.2 证明volatile可见性 ,我们在 int number前面加上volatile关键字

volatile int number = 0;

在这里插入图片描述
此时main线程由可见性及时知道了主内存中的number值发生了修改,跳出了循环。

不保证原子性

代码证明不保证原子性

class MyDate { 
   
    volatile int number = 0;
    public void add() { 
   
        number++;
    }
}

//2.1证明原子性
//原子性是指 完整性,不可分割,就是说某个线程执行时不可分割或被插队。
public class VolatileDemo { 
   
    public static void main(String[] args) { 
   
        MyDate myDate = new MyDate();
        for (int i = 0; i < 10; i++) { 
   
            new Thread(() -> { 
   
                for (int j = 0; j < 1000; j++) { 
   
                    myDate.add();
                }
            }, String.valueOf(i)).start();
        }
        // 等待线程计算完成后 由main线程取最终结果 10*1000 = 1万
        while (Thread.activeCount()>2){ 
    //mian线程和GC后台线程
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t sum value="+myDate.number);
    }

代码for出10个线程,每个线程做1000次add操作。正常来说10个线程操作完成之后number应该等于10000
在这里插入图片描述

但是程序没次运行都会得到不同的结果,而且小于10000.
是由于number++这个操作在java中是线程不安全的。

n++这个操作在字节码中分为了三个步骤

getfield —- 拿到原始的值n
iadd ——– 进行+1操作
putfield —–把累加后的值写回

当在多线程环境中,如果多个线程都拿到了原始的n,运算完成后在写回数值之前线程被挂起或阻塞,线程恢复后来不及收到可见性通知就向主内存写下了数值,就会发生写覆盖的情况,丢失数据。

导致了在本程序中无法达到10000.

如何解决呢

public synchronized void add() { 
   
        number++;
    }

首先可能想到的就是synchronized同步方法,但是synchronized太重了,一个方法里面就一个++操作如果用synchronized就好比杀鸡用牛刀,大炮打蚊子。需要功能和性能的同时考虑。

使用java.util.concurrent.atomic下的AtomicInteger类
在这里插入图片描述
原子性的增加1 相当于++操作

或者getAndAdd(int delta) 参数填1

 //2.3 原子性int
    AtomicInteger atomicInteger = new AtomicInteger();
    // 原子性方法
    public void addWithAtomic(){ 
   
        atomicInteger.getAndIncrement();
    }

在这里插入图片描述
至于为什么AtomicInteger能保证原子性,后面有机会继续详解。

禁止指令重排

指令重排:计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为编译器优化重排,指令并行的重排,内存系统的重排。

在多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的共享变量的一致性时无法保证的。所以结果无法预测。

int a = 0;
    boolean flag = false;

    public void writer() { 
   
        a = 1;         //语句1
        flag = true;   //语句2
    }

    public void reader() { 
   
        if (flag) { 
        //语句3
            a = a + 1;  //语句4
            System.out.println("value:" + a);
        }
    }

在这里插入图片描述
在这里插入图片描述
volatile 实现了禁止指令重排序优化,从而避免了多线程环境下程序出现乱序执行的现象

实现:在对volatile变量进行写操作时,会在写操作后面加入一条Memory Barriier(内存屏障)告诉内存和CPU,禁止在内存屏障前后的执行指令重排优化


在这里插入图片描述

在这里插入图片描述

volatile 使用场景

DCL(双重检查锁)+ volatile的单例模式

public class Singleton { 
   
    private static Singleton instance = null;
    // + volatile ?
    private Singleton() { 
   
        System.out.println(Thread.currentThread().getName() + "\t Singleton的构造方法");
    }

    //DCL Double check lock
    public static Singleton getInstance() { 
   
        if (instance == null) { 
   
            synchronized (Singleton.class) { 
   
                if (instance == null) { 
   
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

如果不加volatile

由于

instance = new Singleton();

要分为3步骤执行

1.分配对象内存空间
2.初始化对象
3.完成对象的引用

步骤2和步骤3不存在数据依赖关系,所以可能发生重排序,1->3->2 此时的引用对象可能还没有完成初始化

当一条线程访问instance不为null时,由于instance实例还没有初始化完成,就造成了线程安全问题。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/156478.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • linux下chmod +x的意思?为什么要进行chmod +x

    linux下chmod +x的意思?为什么要进行chmod +x上周在工作中接触到chmod+x这个命令,如下图:首先对start.sh这个启动文件删除,然后使用rz命令上传了新的start.sh,然后发现还有进行下一步,chmod+xstart.sh这一步是什么意思呢?经过上网查询(说的比较复杂,引申太多)和咨询我们研发(还是研发说的通俗易懂)chmod+x的意思就是给执行权限LINUX下不同的文件类型有不同的颜色,这里…

    2022年7月12日
    33
  • Tomcat配置环境变量

    Tomcat配置环境变量Tomcat是目前比较流行的开源且免费的Web应用服务器,在我的电脑上第一次安装Tomcat,再经过网上教程和自己的摸索后,将这个过程重新记录下来,以便以后如果忘记了可以随时查看。注意:首先要明确一点,Tomcat与Java密切相关,因此安装使用之前要先安装JDK并设置JDK的环境变量,由于机子上已经安装好了JDK,也设置好了JDK环境变量,因此这里不再过多叙述,只说明我设置好的环境变量:JAV…

    2022年5月4日
    29
  • 双机流水作业调度问题——Johnson算法「建议收藏」

    双机流水作业调度问题——Johnson算法「建议收藏」概述流水作业是并行处理技术领域的一项关键技术,它是以专业化为基础,将不同处理对象的同一施工工序交给专业处理部件执行,各处理部件在统一计划安排下,依次在各个作业面上完成指定的操作。流水作业调度问题是一个非常重要的问题,其直接关系到计算机处理器的工作效率。然而由于牵扯到数据相关、资源相关、控制相关等许多问题,最优流水作业调度问题处理起来非常复杂。已经证明,当机器数(或称工序数)大于等于3时,流水作业调度问题是一个NP-hard问题(e.g分布式任务调度)。粗糙地说,即该问题至少在目前基本上没有可能找到多项

    2025年5月24日
    0
  • C#基本概念列举说明建议收藏

    1.关键字在C#代码中常常使用关键字,关键字也叫保留字,是对C#有特定意义的字符串。关键字在VisualStudio环境的代码视图中默认以蓝色显示。例如,代码中的using、namespace

    2021年12月21日
    40
  • dsp28335复位电路_28335串口不能中断

    dsp28335复位电路_28335串口不能中断0前言本期实验目标:采用外部中断方式响应按键触发,实现LED电平反转。外部中断是DSP十分常用的功能,通常用来响应一些控制操作,比如判断按键是否按下,传感器是否接收到信号等等。那么通过该例程,大家则可以快速学会使用外部中断的功能!本节仍然将分为硬件部分、软件部分和实验展示三个方面进行介绍。1硬件部分DSP28335支持XINT1-XINT7和XNMI共8路外部中断源,其中中断源XINT1/2和XNMI可以设定为从GPIO端口A的任意一个管脚输入,即GPIO0-GPIO31。而XINT3/4/5/

    2022年9月6日
    2
  • 我是如何利用“王宝强离婚”事件来吸粉的

    我是如何利用“王宝强离婚”事件来吸粉的

    2021年9月17日
    39

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号