【Java基础】:volatile实现可见性的原理

1. 引言

在java并发编程中,一定绕不开volatile、synchronized和lock几个关键字,其中volatile关键字是用来解决共享变量(类成员变量、类的静态成员变量等)的可见性问题的,非共享变量(方法的局部变量)是分配在JVM虚拟机的栈中,是线程私有的,不涉及可见性问题。那么什么是可见性?

2. 什么叫做可见性

可见性:在JAVA规范中是这样定义的:java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。通俗的将就是如果有一个共享变量N,当有两个线程T1、T2同时获取了N的值,T1修改N的值,而T2读取N的值。那么可见性规范要求T2读取到的必须是T1修改后的值,而不能在T2读取旧值后T1修改为新值。volatile关键字修饰的共享变量可以提供这种可见性规范,也叫做读写可见。那么底层实现是通过机制保证volatile变量读写可见的?

3. Volatile的实现机制

在说这个问题之前,我们先看看CPU是如何执行java代码的。
在这里插入图片描述
首先编译之后Java代码会被编译成字节码.class文件,在运行时会被加载到JVM中,JVM会将.class转换为具体的CPU执行指令,CPU加载这些指令逐条执行。
在这里插入图片描述
以多核CPU为例(两核),我们知道CPU的速度比内存要快得多,为了弥补这个性能差异,CPU内核都会有自己的高速缓存区,当内核运行的线程执行一段代码时,首先将这段代码的指令集进行缓存行填充到高速缓存,如果非volatil变量当CPU执行修改了此变量之后,会将修改后的值回写到高速缓存,然后再刷新到内存中。如果在刷新会内存之前,由于是共享变量,那么CORE2中的线程执行的代码也用到了这个变量,这是变量的值依然是旧的。volatile关键字就会解决这个问题的,如何解决呢,首先被volatile关键字修饰的共享变量在转换成汇编语言时,会加上一个以lock为前缀的指令,当CPU发现这个指令时,立即做两件事:

  1. 将当前内核高速缓存行的数据立刻回写到内存;
  2. 使在其他内核里缓存了该内存地址的数据无效。

第一步很好理解,第二步如何做到呢?
MESI协议:在早期的CPU中,是通过在总线加LOCK#锁的方式实现的,但这种方式开销太大,所以Intel开发了缓存一致性协议,也就是MESI协议,该解决缓存一致性的思路是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,那么他会发出信号通知其他CPU将该变量的缓存行设置为无效状态。当其他CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会从新从内存中读取这个变量。

4. 使用volatile的好处

从底层实现原理我们可以发现,volatile是一种非锁机制,这种机制可以避免锁机制引起的线程上下文切换和调度问题。因此,volatile的执行成本比synchronized更低。

5. volatile的不足

使用volatile关键字,可以保证可见性,但是却不能保证原子操作,例如:

public class TestVolatile {
    public volatile int inc = 0;
    public void increase() {
        inc++;
    }
 
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
 
        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println(test.inc);
    }
}

这里我们用10个线程,每个线程+1000,预期应该是10000,实际上编译执行这段代码,输出值都会小于10000。为什么会这样?因为,自增操作并不是原子操作,它含括读,加1,写入工作内存三步操作。这三步是分开操作的,当inc的值为5,thead1执行自增操作,当thread1读到5之后,还没有来得及写入就被阻塞了,那么thead2读取的依然是原值。所以volatile的非锁机制只能保证修饰的变量的可见性,而当对变量进行非原子操作时,volatile就无法保证了。这种时候就需要使用synchronzied或lock。

参考

  1. https://blog.csdn.net/nch_ren/article/details/78924808

猜你喜欢

转载自blog.csdn.net/hxcaifly/article/details/88093099