volatile关键字深度解析

volatile作用

能够保证线程可见性,当一个线程修改共享变量时,能够保证对另一个线程可见性,但是不能够保证共享变量的原子性问题

volatile的特性

可见性:能够保证线程可见性,当一个线程修改共享变量时,能够保证对另外一个线程可见性。
顺序性:程序执行程序按照先后顺序执行,禁止重排序
原子性:原子性在一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰
代码例子

package com.mayikt;

/**
 * @Description:
 * @Author: ChenYi
 * @Date: 2020/07/19 11:37
 **/

public class Thread001 implements Runnable {
    private volatile static boolean flag = true;

    @Override
    public void run() {
        while (flag) {

        }
    }

    public static void main(String[] args) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread thread = new Thread(new Thread001());
        thread.start();
        System.out.println("主线程已经执行完毕");
        flag = false;
    }
}

在这里插入图片描述

在这里插入图片描述

产生可见性的原因

因为我们cpu读取主内存共享变量的数据的时候,效率时非常低的,所以对每个cpu设置对应的高速缓存L1、L2、L3 缓存我们共享变量主内存中的副本。
相当于每个cpu对应共享变量的副本,副本与副本之间可能会存在一个数据不一致性的问题,如:线程B修改了某个副本值,线程A的副本可能看不见,导致可见性问题。

JMM内存模型

主内存

存放共享变量的数据

工作内存

每个cpu对共享变量(主内存)的副本

JMM八大同步规范

(1)lock(锁定):作用于 主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁):作用于 主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于 主内存的变量,把一个变量值从主内存传输到线程的 工作内存中,以便随后的load动作使用
(4)load(载入):作用于 工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于 工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值):作用于 工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(7)store(存储):作用于 工作内存的变量,把工作内存中的一个变量的值传送到 主内存中,以便随后的write的操作
(8)write(写入):作用于 工作内存的变量,它把store操作从工作内存中的一个变量的值传送到 主内存的变量中
在这里插入图片描述

Volatile的底层实现原理

通过汇编lock前缀指令触发底层锁的机制
锁的机制有两种:总线锁/MESI缓存一致性协议
总线的作用:主要帮助我们解决多个不同cpu之间三级高速缓存之间数据同步

总线锁

当一个cpu访问到我们主内存中的数据的时候,往总线发出一个lock锁的信号,其他线程不能够对该主内存做任何操作,变为阻塞状态,但该模式存在非常大的缺陷,没有真正发挥出cpu多核的好处

MESI协议

  1. M修改(Modified)这行数据有效,数据被修改了,和主内存中的数据不一致,如果当前cpu副本数据与主内存中的数据不一致的情况下,则当前cpu状态为M
  2. E独享、互斥(Exclusive)这行数据有效,数据和主内存中的数据一致,当只有一个cpu线程的情况下,cpu副本数据与主内存数据如果保持一致的情况下,则该cpu状态为E状态 独享
  3. S 共享(Shared)这行数据有效,在多个cpu线程的情况下,每个cpu副本之间数据与主内存中的一致,则当前cpu状态为S
  4. I 无效(Invalid)无效,总线嗅探机制发现,如果cpu存在m的情况下,则会将cpu的状态为i,该cpu缓存会主动获取主内存的数据同步更新。

为什么volatile不能保证原子性

volatile为了能够保证数据的可见性,但是不能够保证原子性,因为需要及时的将工作内存的数据刷到主内存中,导致其他的工作内存的数据变为无效状态,下面的代码的count的结果小于10000以内。

package com.mayikt;

import java.util.ArrayList;
import java.util.List;

/**
* @Description:
* @Author: ChenYi
* @Date: 2020/07/19 16:51
**/

public class VolatileAtomThread {
   private static volatile int count;

   public static void create() {
       count++;
   }

   public static void main(String[] args) {
       List<Thread> threads = new ArrayList<>();
       for (int i = 0; i < 10; i++) {
           Thread thread = new Thread(() -> {
               for (int j = 0; j < 1000; j++) {
                   create();
               }
           });
           threads.add(thread);
           thread.start();
       }
       threads.forEach(e -> {
           try {
               e.join();
           } catch (InterruptedException ex) {
               ex.printStackTrace();
           }
       });
       System.out.println(count);
   }
}

什么是重排序

Java内存模型允许编译器和处理器对指令代码实现重排序提高运行的效率,只会对不存在的数据依赖的指令实现重排序,在单线程的情况下重排序能够保证最终执行的结果与程序顺序执行结果一致性

重排序产生的原因

当我们的cpu写入缓存的时候发现缓冲区正在被其他cpu占有的情况下,为了能够提高cpu处理的性能,将后面的读缓存命令优先执行。
注意:不是随便重排序,需要遵循as-ifserial语义
as-ifserial:不管怎么重排序(编译器和处理器为了提高并行的效率),单线程程序执行结果不会发生改变的,编译器和处理器不会对存在数据依赖的关系操作做重排序
cpu指令重排序优化的过程存在问题
as-ifserial单线程程序执行结果不会发生改变的,但是在多线程的情况下,指令逻辑无法分辨因果关系,可能会存在一个乱序中心问题,导致程序执行结果错误。

内存屏障解决重排序

处理器提供了两个内存屏蔽指令,解决以上存在的问题
1.写内存屏障:在指令后插入Stroe Barrier ,能够让写入缓存中的最新数据更新写入主内存中,让其他线程可见。
这种强制写入主内存,这种现实调用,Cpu就不会因为性能的考虑对指令重排序。
2.读内存屏障:在指令前插入load Barrier ,可以让告诉缓存中的数据失效,强制重新主内存加载数据
强制读取主内存,让cpu缓存与主内存保持一致,避免缓存导致的一致性问题。

双重检验锁为什么需要加上volatile

public class Singleton03 {
    private static volatile Singleton03 singleton03;

    public static Singleton03 getInstance() {
        // 第一次检查
        if (singleton03 == null) {
            //第二次检查
            synchronized (Singleton03.class) {
                if (singleton03 == null) {
                    singleton03 = new Singleton03();
                }
            }
        }
        return singleton03;
    }

    public static void main(String[] args) {
        Singleton03 instance1 = Singleton03.getInstance();
        Singleton03 instance2 = Singleton03.getInstance();
        System.out.println(instance1==instance2);
    }
}

因为我们在new操作的时候,存在重排序的问题
步骤:
1.分配对象的内存空间
2.调用构造函数初始化
3.将对象复制给变量
注意:第二步和第三步流程存在重排序,线程1可能会先执行第三步,然后在执行第二步,这个时候线程2获取到该对象不为空,但是该对象还没有执行构造函数初始化,这个时候就会报错,因为拿到的是一个不完整的对象,所以就需要用volatile关键字来禁止重排序。

volatile存在的伪共享的问题

cpu会以缓存行的形式读取主内存中数据,缓存行的大小为2的幂次数字节,一般的情况下是为64个字节。
如果该变量共享到同一个缓存行,就会影响到整体性能,如:线程1修改了long类型变量A,long类型定义变量占用8个字节,由于缓存一致性协议,线程2的变量A副本会失效,线程2读取主内存中的数据的时候,以缓存行的形式读取,无意间将主内存中的共享变量B也读取到内存中,而主内存中的变量B没有发生变化。
在这里插入图片描述

解决缓存行解为共享问题

  1. 使用缓存行填充方案避免为共享
  2. 在类中加上@sun.misc.Contended

synchronized 与volatile存在的区别

  1. Volatile保证线程可见性,当工作内存中副本数据无效之后,主动读取主内存中数据
  2. Volatile可以禁止重排序的问题,底层内存屏障。
  3. Volatile不会导致线程阻塞,不能够保证线程安全问题,synchronized 会导致线程阻塞 能够保证线程安全问题。
    参考:蚂蚁课堂
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值