JAVA Volatile关键字 使用以及源码分析

目录

知识前提

JMM 概念和原理 、三大特性 、 Happens-Before 先行发生原则_m0_71149992的博客-优快云博客

Volatile特点

被volatile修改的变量有2大特点

内存屏障

​编辑

什么是内存屏障

内存屏障分类

深入内存屏障

内存屏障策略

volatile特性 

 volatile特性 - 保证可见性

volatile变量读写过程

 volatile特性 - 不保证原子性

不保证原子性的原因

volatile特性 - 禁止指令重排

volatile底层实现

volatile使用场景

 单一赋值可以,but含复合运算赋值不可以(i++之类)

判断业务是否结束

开销较低的读,写锁策略

单列模式 DCL双端锁的发布

总结

三句话总结 


知识前提

Juc11_Java内存模型之JMM、八大原子操作、三大特性、读写过程、happens-before_所得皆惊喜的博客-优快云博客_java11内存模型

JMM 概念和原理 、三大特性 、 Happens-Before 先行发生原则_m0_71149992的博客-优快云博客

Volatile特点

被volatile修改的变量有2大特点


①. 特点:可见性、有序性、不保证原子性

②. volatile的内存语义

1、当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
2、当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
3、所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取

volatile凭什么可以保证可见性和有序性

内存屏障


 

有序性

不存在数据依赖关系  可以重排序

      存在数据依赖关系 禁止重排序

什么是内存屏障

①. 内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性

②. 内存屏障之前的所有写操作都要回写到主内存
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)

③. 一句话:对一个volatile域的写, happens-before于任意后续对这个volatile域的读,也叫写后读

 

内存屏障分类

粗分

细分

源码分析内存屏障指令的由来
(1). IDEA工具里面找Unsafe.class

在这里插入图片描述

(2). Unsafe.java
在这里插入图片描述(3). Unsafe.cpp
在这里插入图片描述(4). OrderAccess.hpp
在这里插入图片描述(5). orderAccess_linux_x86.inline.hpp
在这里插入图片描述

所以最终细分有4分钟

LoadLoad 

StoreStore 

LoadStore

StoreLoad 

在这里插入图片描述

 在这里插入图片描述

深入内存屏障

 

 happens-before之volatile变量规则

内存屏障策略

读屏障

 读插入内存屏障的指令序列示意图

 写屏障

写插入内存屏障的指令序列示意图

volatile特性 

 volatile特性 - 保证可见性

保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可以看到

代码如下

public class VolatileSeeDemo
{
    static          boolean flag = true;       //不加volatile,没有可见性

   public static void main(String[] args)
    {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t"+"---come in");
            while(flag)
            {

            }
            System.out.println("t1 over");
        },"t1").start();

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

      flag =false;

        System.out.println(Thread.currentThread().getName()+"\t 修改完成 flag: "+ flag);
    }
}

 测试结果

 程序并没有停止

原因:   主线程 将 flag修改为 false 没有将最新的数据刷新到主内存,也没有通知其他线程flag已发生改变,让他重新去主内存拉取最新数据

修改代码

public class VolatileSeeDemo
{
    static    volatile      boolean flag = true;       //不加,没有可见性

   public static void main(String[] args)
    {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t"+"---come in");
            while(flag)
            {

            }
            System.out.println("t1 over");
        },"t1").start();

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

      flag =false;

        System.out.println(Thread.currentThread().getName()+"\t 修改完成 flag: "+ flag);
    }
}

结果

上面问题 原理解释:

volatile变量读写过程

 

 

 volatile特性 - 不保证原子性

测试代码如下

理想 结果 是 10000

class MyNumber
{
     volatile int number = 0;

    public void addPlusPlus()
    {
        number++;
    }
}

public class VolatileNoAtomicDemo
{
    public static void main(String[] args) throws InterruptedException
    {
        MyNumber myNumber = new MyNumber();

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName() + "\t" + myNumber.number);
    }
}

测试结果

 每次运行结果都不一样

读取赋值一个变量的情况

不保证原子性的原因

可以在 《深入jvm虚拟机 》第12章3.3可以查看

从底层来看i++ 问题

结论 

 在这里插入图片描述

volatile特性 - 禁止指令重排

指令重排是什么

不存在数据依赖性  可以重排

存在数据依赖性  禁止重排

volatile底层实现

volatile底层是根据内存屏障实现的

volatile有关的禁止指令重排的行为

四大内存屏障插入情况

代码说明

添加 这个 StoreStore  是禁止将 这个flag =true 重新排序  ,如果没有 这个volatile 修饰 可能 会将 flag =true  会优化到第一行 ,结果有多个线程调用这个read 方法 if(flag)为true  但是 i 还没有进行赋值 ,此时 结果与预想结果有误 ,造成语义错误

StoreLoad 指令 就是禁止 flag =true  与下面的 volatile 读或者写重排序

添加 LoadLoad指令 只要有 volatile 写 此时主内存的数据已经是最新的 必须去从主内存中拿最新数据  只有我读完了 确认了在进行后面操作

 
添加 LoadStore 指令  只有volatile读完了 以后再 进行后面操作  从而保证语义正确性和一致性

volatile使用场景

 单一赋值可以,but含复合运算赋值不可以(i++之类)

volatile int a = 10
volatile boolean flag = false

判断业务是否结束

public class UseVolatileDemo{
    private volatile static boolean flag = true;
    public static void main(String[] args){
        new Thread(() -> {
            while(flag) {
                //do something......
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}

开销较低的读,写锁策略

public class UseVolatileDemo{
    /**
     * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
     * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     */
    public class Counter{ 
        private volatile int value;
        public int getValue(){
            return value;   //利用volatile保证读取操作的可见性
         }
        public synchronized int increment(){
            return value++; //利用synchronized保证复合操作的原子性
         }
    }
}
 

单列模式 DCL双端锁的发布

原始代码

在单线程看待这个问题

没加volatile可能出现重排序

在多线程看待这个问题

也就是  第二步和第三步执行顺序 对调了  导致对象还没有初始化完成  就引用指向刚开辟的空间 导致其他现在得到一个null

问题解决

public class SafeDoubleCheckSingleton{
    //通过volatile声明,实现线程安全的延迟初始化。
    private volatile static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                                      //原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

有不加volatile的方法解决单例模式的懒汉线程问题 

采用静态内部类的方式实现

public class SingletonDemo {
    private SingletonDemo() { }

    private static class SingletonDemoHandler {
        private static SingletonDemo instance = new SingletonDemo();
    }

    public static SingletonDemo getInstance() {
        return SingletonDemoHandler.instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                SingletonDemo instance = getInstance();
                // 可以知道这里获取到的地址都是同一个
                System.out.println(instance);
            },String.valueOf(i)).start();
        }
    }
}

采用枚举 也可以解决线程安全问题 

public enum Singleton {
    INSTANCE;

    public void sayHello() {
        System.out.println("Hello World");
    }
}

总结

volatile 可见性

volatile 禁止重排

1  写指令

2 读指令

volatile 与内存屏障

内存屏障是什么

内存屏障能干啥

内存屏障四大指令

三句话总结 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值