Volatile应用与底层原理

Volatile应用与底层原理

一、Java内存模型

由于CPU与内存之间速度的不一致所以设置了一种中间机制——缓存,不同的操作系统他们关于缓存的实现又是多样的,Java为了屏蔽操作系统的一些底层差异就设计了一种操作的规范——JMM(Java内存模型),JMM并不是物理存在的,他是逻辑上的一种概念。JMM有三大特性——可见性、原子性、有序性。个人感觉这与数据库底层的原理实际上有相似之处,说白了都是对数据的操作呗。源码到最终的执行会经过编译器优化重排、指令并行重排、内存系统的重排。那么应该按照什么样的顺序来判断重排以及如何重排呢?他就是Happens-Before原则!

1、Happens-Before原则

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前(这里说的应该是写吧);两个操作之间如果重排序之后的执行结果与按照happens-before关系来执行的结果一直,那么这种重排序就被允许并不非法。

  • **次序原则:**一个线程内,按照代码的顺序写在前面的操作先行于写在后面的操作发生
  • 锁定原则:对于同一把锁的获取在释放之后
  • Volatile原则:被他修饰的禁止重排序 ❓
  • 传递原则:如果a要在b之前,b要在c之前,那么a就要在c之前
  • 线程启动原则:线程中的代码发生在start()方法之后
  • 线程中断原则:对线程interrupt方法的调用先行发生于中断线程的代码检测到中断事件的发生
  • 线程终止原则:线程中的所有操作都先行发生于对线程的终止检测、我们可以通过isAlive()等手段检测线程是否已经终止执行
  • 对象终结原则:一个对象的初始化完成先行发生于他的finalize(进行垃圾回收的时候会执行一些这个方法,他可以做线程最后的逃逸)方法的开始,垃圾回收应该发生在对象创建完成之后

二、Volatile的概况

​ 线程在对数据进行计算的时候并不是直接操作内存中的数据而是会把内存中的数据放在线程专属的工作内存中,在放入的过程中为了提高效率,中间还会经过缓存。但是这就导致了在多线程并发的情况下一个线程操作的情况不能够及时的被其他线程感知到,Volatile就是为了解决这个问题的。Volatile拥有可见性、有序性两大特性,但是他不能够保证原子性。

可见性是因为被Volatile修饰的关键字写操作发生后都被要求写回到主内存,其他线程读取的时候也要求必须从主内存中读取。

1、可见性问题代码示例

static int count = 0;
@Test
public void test02() throws InterruptedException {
    for (int i = 0; i < 10;i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                count++;
            }
        }).start();
    }
    TimeUnit.SECONDS.sleep(2);
    System.out.println("Result: "+count);
}
//本机使用的是JDK8的版本,可见性的问题并没有表现出来,这可能是底层进行了优化

2、非原子性代码示例

static int count = 0;
@Test
public void test02() throws InterruptedException {
    for (int i = 0; i < 10;i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                count++;
            }
        }).start();
    }
    TimeUnit.SECONDS.sleep(2);
    System.out.println("Result: "+count);
}
//最后输出的结果按理说应该是一直等于10000的但是事实并非如此

关于可见性问题就不说了,Volatile的出现就是为了解决可见性问题的,只需要在操作的属性上加上此关键字即可。原子性出现的问题是因为i++虽然在源码层面来说是一条语句但是在底层中他是由多个步骤组成。这几个步骤并不构成原子性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2eqNmnpx-1660283497472)(F:\typroa\aimages\image-20220811140049036.png)]
lock、unlock:都是作用与主内存,提供读锁以及释放锁的操作

read、write可以看做是一对他们都是对主内存进行操作,将主内存中的数据读取出来或是写数据到主内存

load、store:是对工作内存进行操作,将主内存中通过read中读取到的数据加载放入到工作内存以及将工作内存中的数据载入到主内存

use、assign:作用与工作内存以及cpu之间,当CPU计算使用到一个数据的时候就会通过use来将工作内存中数据放入到cpu、当源码层面执行一个复制的操作时候就会调用assign的这个操作

volatile不能够保证原子性是因为除过与锁相关的六个操作都是相互独立的,并发的时候可能一个t1刚在工作内存中完成了一个++的操作还没有刷到主内存,这时候一个线程t2将自己的计算结果刷到了主内存。他们操作的数据被Volatile修饰了,所以说t2会将他修改的结果通知给t1,而造成t1内存数据的失效实现一个可见性(自己并不认为,可见性机制是这样实现的,而是他们的每次读都会从主内存中获取实现,但是这个是看B站视频时候一个老师的说法,或许是为了便于理解吧)。通过加锁就可以保证因为非原子性而引发的问题,这比较容易理解,加锁的期间只有一个线程可以进入这一段指令的执行,只有他执行完其他的线程才能够执行这一段指令,也就可以说实现了代码层面的原子性。

三、内存屏障

内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些指令Volatile实现了Java内存模型中的可见性和有序性(禁止重排)。从大类上来分内存屏障分为读屏障(在读操作之前插入)、写屏障(在写之前插入)

  • 写屏障:当看到Store屏障指令的时候,就必须把该指令之前所有的写入指令执行完毕同步到主内存,才能继续往下执行;
  • 读屏障:在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据,这就要求读屏障之后的读操作都要在读屏障之后执行

当去研究字节码层面的内容时会发现,内存屏障又可以经过组合分为四种,StoreStore、LoadLoad、StoreLoad、LoadStore。
在这里插入图片描述
上面图片介绍的是一个是四种屏障指令插入的一个时机,通过这样一种插入机制就划定了如下的一个规则:
在这里插入图片描述
总的来说就是Volatile修饰字段的读写是肯定不能够进行重排序的,volatile修饰字段的读两个屏障会加到后面这样也抑制了后面普通读写的重排序,当Volatile修饰字段的进行写操作的时候前后都加了屏障指令,所以他前面的普通读写也都不可以。

说到这里我好想是唔明白了,不管是上面四种指令的那一种加上之后实际是对所有的读写操作是有抑制作用的。有一个例外那就是StoreLoad指令,他抑制的知识后面的Volatile的读写。我们或许可以说实际这四个指令也就是一个标记的作用,他们标记之后由更底层的操作来决定接下来如何做

四、Volatile的应用

1、单一赋值不需要依赖原来值

这个也一定程度验证了上面说的关于可见性实现的猜想

2、状态标志、判断业务是否结束

上面关于可见性的场景示例

3、开销较低的读、写锁策略

public class VolatileDemo2 {
    private static volatile int count = 0;
    public static int getCount() {
        //读操作的数据安全性是通过volatile操作实现的,每一次都会是从主内存获取
        return count;
    }
    public static synchronized void setCount(){
        //负债操作的原子性
        count += 10;
    }
}

4、DCL双端锁

这里用volatile关键字主要是利用他的防指令重排。

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

singleton = new VolatileSingle();这个操作分为三步:

  • 分配空间
  • 初始换对象
  • 设置single指向刚刚分配的地址

当指令重排序为132的时候,就可能发生初始化还未完成但是已经被其他的线程获取到了,这时候放回的值为null

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值