volatile关键字

1.volatile简介

synchronized是阻塞式同步,线程竞争下升级为重量级锁

volatile是轻量级锁被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

2.volatile的作用

2.1 保证内存可见性

在这里插入图片描述

线程的共享变量都存储在主存中,每一个线程都有独有的工作内存,线程操作的变量是从主存中拷贝过来的副本放入工作内存,有一个工作内存中的副本发生变化就会刷回主内存中,其他线程中该变量的缓存作废,当要其他线程需要用时从主内存中读取该变量的值。

MESI缓存一致性协议

volatile可见性是通过汇编加上Lock前缀指令触发底层的MESI缓存一致性协议实现,该Lock指令主要有2个作用:

  1. 当前处理器缓存行的数据写回系统内存
  2. 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

volatile实现原理:

  1. Lock前缀的指令会引起处理器缓存写回内存;
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
  3. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

MESI主要有以下4种状态:

状态描述
M 修改(Modified)此时缓存行中的数据与主内存中的数据不一致,数据只存在于本工作内存中。其他线程从主内存中读取共享变量值的操作会被延迟执行,直到该缓存行将数据写回到主内存后
E 独享(Exclusive)此时缓存行中的数据与主内存中的数据一致,数据只存在于本工作内存中。此时会监听其他线程读主内存中共享变量的操作,如果发生,该缓存行需要变成共享状态
S 共享(Shared)此时缓存行中的数据与主内存中的数据一致,数据存在于很多工作内存中。此时会监听其他线程使该缓存行无效的请求,如果发生,该缓存行需要变成无效状态
I 无效(Invalid)此时该缓存行无效
2.2禁止指令重排序

为保证执行上的效率,JVM(包括CPU)可能会对指令进行重排序

public class Singleton {
 
    private volatile static Singleton instance;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

由上可以看到,instance变量被volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性。这是因为“instance = new Singleton();”这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:

  1. 为instance分配内存
  2. 初始化instance
  3. 将instance变量指向分配的内存空间

由于JVM可能存在重排序,上述的二三步骤没有依赖的关系,可能会出现先执行第三步,后执行第二步的情况。也就是说可能会出现instance变量还没初始化完成,其他线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后,可以保证instance变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,这样就不会出现问题。

内存屏障

volatile有序性通过内存屏障实现,JVM和CPU都会对指令做重排序优化,所以在指令间插入一个屏障点,告诉JVM和CPU不能重排优化。具体分为 读读、读写、写读、写写4种屏障。

屏障点描述
每个volatile写的前面插入一个store-store屏障禁止上面的普通写和下面的volatile写重排序
每个volatile写的后面插入一个store-load屏障禁止上面的volatile写与下面的volatile读/写重排序
每个volatile读的后面插入一个load-load屏障禁止下面的普通读和上面的volatile读重排序
每个volatile读的后面插入一个load-store屏障禁止下面的普通写和上面的volatile读重排序
2.3 不保证原子性
public class Test {
 
    private static CountDownLatch countDownLatch = new CountDownLatch(1000);
    private volatile static int num = 0;
 
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                try {
                    num++;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();
        System.out.println(num);
    }
}

使用volatile关键字修饰num,开启1000个线程但是num的值最后< 1000。假设2个线程A、B,线程A获取num=1,执行num++时候发现线程B已经将num修改为4,所以根据MESI协议A的num值失效,重新从主内存中读取最新值

上述过程A、B线程都做了+1操作,但是只有B成功了,也就是做了2次num++,但只加了一次,所以num终值< 1000

解决原子性问题

使用synchronized重量级锁或者使用JUC包下的AtomicInteger

public class Test {
 
    private static CountDownLatch countDownLatch = new CountDownLatch(1000);
    private static AtomicInteger  num            = new AtomicInteger();
 
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> {
                try {
                    num.getAndIncrement();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();
        System.out.println(num);
    }
}

3 volatile和synchronized的区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、类级别的
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

4 总结

volatile关键字是轻量级锁

主要作用:

  1. 可见性(MESI缓存一致性协议)
  2. 有序性,禁止指令重排(内存屏障)

适用场景:一个线程写,多个线程读
注意:volatile不是原子性操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值