专栏首页Jimoer你说一下对Java中的volatile的理解吧

你说一下对Java中的volatile的理解吧

前言

volatile相关的知识其实自己一直都是有掌握的,能大概讲出一些知识,例如:它可以保证可见性禁止指令重排。这两个特性张口就来,但要再往深了问,具体是如何实现这两个特性的,以及在什么场景下使用volatile,为什么不直接用synchronized这种深入和扩展相关的问题,就回答的不好了。因为volatile是面试必问的知识,所以这次准备把这部分知识也给啃掉。

系统处理效率与Java内存模型

在计算机中,每条程序指令都是在CPU中执行的,而CPU执行指令的数据都是临时存储在内存中的,但是CPU的执行速度远超内存的读取速度,如果所有的CPU指令都是通过内存来读取数据的话那么将大大的降低了系统的处理效率,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲

将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后,在从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

虽然说增加了高速缓存提高了CPU的处理效率,但是也带来了新的问题 :

现代计算机都是多核CPU,一开始,内存中的变量A的值是1,第一个CPU读取了数据,第二个CPU也将数据读取到了自己的高速缓存当中,当第一个CPU对变量A进行加1操作时,变量A的值变成了2,然后将将变量A的值写回内存中,这时第二个CPU也对变量A进行加1操作时,由于第二个CPU中高速缓存中的值还是1,所以加1操作后的结果为2,然后第二个CPU又将变量A的值同步回内存中,这样就导致执行了两次加1操作后,变量A的值最终是2,而不是3。 这种被多个CPU访问的变量,通常称为共享变量。 而产生的上面的问题,就是引入了高速缓存后的,主内存和缓存内容不一致的问题。 因为每个处理器有自己的高速缓存,但是它们又共享同一块主内存,所以必然会出现主内存不知该以哪个高速缓存中的变量为准的情况。

上面这个缓存不一致的问题,我们先记下来,继续来看Java内存模型,其实Java内存模型描述的上面讲的计算机系统高速缓存和内存之间的关系类似。

Java内存模型描述了,各种变量的访问规则,以及将变量存储到内存和从内存读取变量的这种底层细节。

在Java内存模型中关注的变量都是共享变量(实例变量、类变量)。 所有的共享变量都是存储在主内存中的,但是每个线程在访问变量的时候也都会在自己的工作内存处理器高速缓存)中保留一份共享变量的副本。

Java内存模型(Java Memory Model,简称JMM)规定:

线程对变量的所有操作(读,写)都必须在工作内存中进行,不能直接操作主内存中的数据。 不同线程之间 也不能直接访问对方工作内存中的变量,线程间的变量值传递必须通过主内存进行中转传递。 在JMM中工作内存和主内存的关系如下图:

Volatile的可见性(保证立即可见)

继续我们上面的缓存一致性的问题,这个问题,在Java内存模型中,就是可见性的问题,即一个线程修改了共享变量的值,对另一个线程来说是不是立即可见的。如果不是立即可见的,那么就会出现缓存一致性的问题,如果是立即可见的,那么另一个线程在进行操作的时候,拿到的变量值就是最新的。就可以解决可见性的问题。

那么怎么解决可见性问题呢?

  • 方案一:加锁

将共享变量加锁,无论是synchronized还是Lock都可以,加锁达到的目的是在同一时间内只能有一个线程能对共享变量进行操作,就是说,共享变量从读取到工作内存到更新值后,同步回主内存的过程中,其他线程是操作不了这个变量的。这样自然就解决了可见性的问题了,但是这样的效率比较低,操作不了共享变量的线程就只能阻塞。

  • 方案二:volatile修饰修饰共享变量

当一个共享变量被volatile修饰后,会保证每个线程将变量修改后的值立即同步回主内存中,当其他线程有需要读取变量时会读取到最新的变量值。

那么volatile做了些什么操作就能解决可见性的问题呢?

被volatile修饰的变量,在被线程操作时,会有这样的机制:

就是线程对变量操作时会从主内存中读取到自己的工作内存中,当线程对变量进行了修改后,那么其他已经读取了此变量的线程中的变量副本就会失效,这样其他线程在使用变量的时候,发现已经失效,那么就会去主内存中重新获取,这样获取到的就只最新的值了。

那么volatile这个关键字是如何实现这套机制的呢?

因为一台计算机有多台CPU,同一个变量,在多个CPU中缓存的值有可能不一样,那么以谁缓存的值为准呢?

既然大家都有自己的值,那么各个CPU间就产生了一种协议,来保证按照一定的规律为准,来确定共享变量的准确值,这样各个CPU在读写共享变量时都按照协议来操作。

这就是缓存一致性协议。

最著名的缓存一致性协议就是Intel的MESI了,说MESI时,先解释一下,缓存行:

缓存行(cache line):CPU高速缓存的中可以分配的最小存储单位,高速缓存中的变量都是存在缓存行中的。

MESI的核心思想就是,当CPU对变量进行写操作时发现,变量是共享变量,那么就会通知其他CPU中将该变量的缓存行设置为无效状态。当其他CPU在操作变量时发现此变量在的缓存行已经无效,那么就会去主内存中重新读取最新的变量。

  • 那么其他CPU是如何发现变量被修改了的呢?

因为CPU和其他部件的进行通信是通过总线来进行的,所以每个CPU通过嗅探总线上的传播数据,来检查自己缓存的值是不是过期了,当处理器发现自己换成行对应的内存地址被修改后,就会将自己工作内存中的缓存行设置成无须状态,当CPU对此变量进行修改时会重新从系统主内存中读取变量。

Volatile的有序性(禁止指令重排)

一般来说,我们写程序的时候,都是要把先代码从上往下写,默认的认为程序是自顶向下顺序执行的,但是CPU为了提高效率,在保证最终结果准确的情况下,是会对指令进行重新排序的。就是说写在前的代码不一定先执行,在后面的也不一定晚执行。

举个例子:

int a = 5; // 代码1
int b = 8; // 代码2
a = a + 4;	// 代码3
int c = a + b;	// 代码4

上面四行代码的执行顺序有可能是

JMM在是允许指令重排序的,在保证最后结果正确的情况下,处理器可以尽情的发挥,提高执行效率。

当多个线程执行代码的时候重排序的情况就更为突出了,各个CPU为了提高自己的效率,有可能会产生竞争情况,这样就有可能导致最终执行的正确性。

所以为了保证在多个线程下最终执行的正确性,将变量用volatile进行修饰,这样就会达到禁止指令重排序的效果(其实也可以通过加锁,还有一些其他已知规则来实现禁止指令重排序,但是我们这里只讨论volatile的实现方式)。

那么volatile是如何实现指令重排序的呢?

答案是:内存屏障

内存屏障是一组CPU指令,用于实现对内存操作的顺序限制。 Java编译器,会在生成指令系列时,在适当的位置会插入内存屏障来禁止处理器对指令的重新排序。

volatile会在变量写操作的前后加入两个内存屏障,来保证前面的写指令和后面的读指令是有序的。

volatile在变量的读操作后面插入两个指令,禁止后面的读指令和写指令重排序。

有序性,不仅只有volatile能保证,其他的实现方式也能保证,但是如果每一种实现方式都要了解那对于开发人员来说就比较困难了。

所以从JDK5就出现了happen-before原则,也叫先行发生原则。 先行发生原则总结起来就是:如果一个操作A的产生的影响能被另一个操作B观察到,那么可以说,这个操作A先行发生与操作B。

这里所说的影响包括内存中的变量的修改,调用了方法,发送量消息等。

volatile中的先行发生原则是,对一个volatile变量的写操作,先行发生于后面任何地方对这个变量的读操作。

Volatile无法保证原子性

原子性,是指一个操作过程要么都成功,要么都失败,是一个独立的完整的。

就像上面说的,如果多个线程对一个变量进行累加,那么肯定得不到想要的结果,因为累加就不是一个原子操作。

要保证累加最终结果正确,要么对累加变量加锁,要么就用AotomicInteger这样的变量。

/**
 * 双重检查加锁式单例
 */
public class DoubleCheckLockSingleton implements Serializable{

    /**
     * 静态变量,用来存放实例。
     */
    private volatile static DoubleCheckLockSingleton doubleCheckLockSingleton = null;

    /**
     * 私有化构造方法,禁止外部创建实例。
     */
    private DoubleCheckLockSingleton(){}

    /**
     * 双重检查加锁的方式保证线程安全又能获得到唯一实例
     * @return
     */
    public static DoubleCheckLockSingleton getInstance(){
        //第一次检查实例是否已经存在,不存在则进入代码块
        if(null == doubleCheckLockSingleton){
            synchronized (DoubleCheckLockSingleton.class){
                //第二次检查
                if(null==doubleCheckLockSingleton){
                    doubleCheckLockSingleton = new DoubleCheckLockSingleton();
                }
            }
        }

        return doubleCheckLockSingleton;
    }

}

为什么要进行双重检查呢? 当第一个线程走到第一次检查时发现对象为空,然后进入锁,第二次就检查时也为空,那么就去创建对象,但是这个时候又来了一个线程来到了第一次检查,发现为空,但是这个时候因为锁被占用,所以就只能阻塞等待,然后第一个线程创建对象成功了,由于对象是被volatile修饰的能够立即反馈到其他线程上,所以在第一个线程释放锁之后,第二个线程进入了锁,然后进行第二次检查时,发现对象已经被创建了,那么就不在创建对象了。从而保证的单例。

还有就是如果创建对象,步骤:

  1. 分配内存空间。
  2. 调用构造器,实例化。
  3. 返回内存地址给引用。

如果这三个指令顺序被重排了,那么当多线程来获取对象的时候就会造成对象虽然实例化了,但是没有分配内存空间,会有空指针的风险。 所以加上了volatile的对象,也保证了在第二次检查时不会被已经在创建过程中的对象有被检测为空的风险。

总结一下

volatile其实可以看作是轻量级的synchronized,虽然说volatile不能保证原子性,但是如果在多线程下的操作本身就是原子性操作(例如赋值操作),那么使用volatile会由于synchronized

volatile可以适用于,某个标识flag,一旦被修改了就需要被其他线程立即可见的情况。也可以修饰作为触发器的变量,一旦变量被任何一个线程修改了,就去触发执行某个操作。

volatile的变量写操作happen-before,后面任何对此volatile变量的读操作。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 你说说对Java中SPI的理解吧

    最近在面试的时候被问到SPI了,没回答上来,主要也是自己的原因,把自己给带沟里去了,因为讲到了类加载器的双亲委派模型,后面就被问到了有哪些是破坏了双亲委派模型的...

    纪莫
  • java面试题:谈谈你对volatile的理解

      最近打算整理下Java面试中频率比较高,相对比较难的一些面试题,感兴趣的小伙伴可以关注下。

    用户4919348
  • Java中的线程池用过吧?来说说你是怎么理解线程池吧?

    Java中的线程池用过吧?来说说你是怎么使用线程池的?这句话在面试过程中遇到过好几次了。我甚至这次标题都想写成【Java八股文之线程池】,但是有点太俗套了。虽然...

    纪莫
  • 面试 || 10 谈一谈对volatile的理解(Java)

    volatile关键字涉及到Java内存模型,在Java内存模型中我们如果读取主内存,他会把数据放到工作内存中,即为本地内存中,那么此时就有可能出现不一致性,如...

    啤酒单恋小龙虾
  • iOS 说一下对 class_rw_t 的理解

    rw代表可读可写,ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中

    赵哥窟
  • 面试官:说说你对Java异常的理解

    不管是工作中还是面试中,异常这一块还是非常重要的。作为Java开发人员来说,学会如何处理异常,哪些异常必须自己处理,哪些异常可以往外抛等等,这些都是必需品。

    田维常
  • 阿里面试:Java的synchronized 能防止指令重排序吗?我犹豫了

    二胖:别说了我就出去试试水,看看现在工作好不好找,顺带出去找找打击,然后才能好好静下心来好好学习。

    java金融
  • 腾讯四面:说说你对Java NIO的通道Channel的理解

    喜欢文章记得关注我点赞哟,感谢支持!重要的事情说三遍,转发+转发+转发,一定要记得转发哦!!!

    Java程序猿
  • 震惊!你在 Java 中所理解的 volatile 在 C++ 中可能是错的?

    近期看到 C++ 标准中对 volatile 关键字的定义,发现和 java 的 volatile 关键字完全不一样,C++ 的 volatile 对并发编程基...

    字节流动
  • 面试官:给我说说你对Java GC机制的理解?

    使用Java快一年时间了,从最早大学时候对Java的憎恶,到逐渐接受,到工作中体会到了Java开发的各种便捷与福利,这确实是一门不错的开发语言。不仅是 ...

    xcbeyond
  • 面试官:说说你对【注解】的理解

    现在已经处于注解盛行时代,注解@Override ,这个注解是再熟悉不过了,还有@Controller、@RequestMapping、@Service.......

    田维常
  • 面试官最爱的volatile关键字

    美的让人心动
  • 谈谈面试官最爱的volatile关键字

    在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模...

    美的让人心动
  • Java面试官最爱的volatile关键字

    在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模...

    Java团长
  • 面试官最爱的volatile关键字

    在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模...

    搜云库
  • Java面试官最爱的volatile关键字

    在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模...

    Spark学习技巧
  • Java-volatile-面试官最喜欢问的关键字之一

     在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以 volatile关键字作为一个小的切入点,往往可以一问到底,把Java内...

    Fisherman渔夫
  • 面试官问我什么是JMM

    JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不...

    java技术爱好者
  • 一个困扰我122天的技术问题,我好像知道答案了。

    这个程序的意思就是定义一个 boolean 型的 flag 并设置为 false。主线程一直循环,直到 flag 变为 true。

    why技术

扫码关注云+社区

领取腾讯云代金券