×

java volatile volatile

java volatile(在Java面试中如何回答好关于volatile的问题)

admin admin 发表于2023-05-06 04:37:14 浏览37 评论0

抢沙发发表评论

本文目录

在Java面试中如何回答好关于volatile的问题

volatile是一个在多线程访问一个变量时保证线程安全的关键字,被volatile修饰的变量在一个线程修改后会立刻让其他线程可见,从jmm角度来看,每个线程都有一个本地内存和主内存,本地内存是线程私有的,主内存是所有线程共享的,当修改一个变量时,第一步会从主内存获取到该变量,保存到自己的本地内存,第二步修改变量,第三步将修改后的变量同步到主内存,这不是一个原子操作,所以这三步会造成线程不安全,而volatile关键字的作用是:在进行这三步的时候,其他线程的都不会从获取本地内存里的变量,而是直接从主内存获取,这样就使得变量的修改在每个线程间可见

Java如何解决可见性和有序性的问题

首先需要了解,为什么会有「可见性」和「时序性」问题,然后我们来看Java是如何解决这两个问题的。

「可见性」和「时序性」问题

导致「可见性」和「时序性」问题的原因有如下几个:

  • 抢占式任务执行:现代CPU执行多任务方式是「抢占式」,它的总控制权在操作系统手中,操作系统会轮流给需要CPU执行的任务分配执行时间片,超过时间后,操作系统会剥夺当前任务的 CPU 使用权,把它排在队列的最后,最后分配时间片……

  • 存储速度差异:各存储执行速度的不同,离CPU越近,存储速度越快,相对的容量就越小。执行程序所需要的数据不可能一次性全部都加载到寄存器中,所以有load与store的过程,影响了所谓的「可见性」

  • 指令重排:大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。

解决方法

解决思路很简单,就是把多线程强制单线程执行。

解决方法无非两种:

  • 内存屏障

先看下JVM的内存模型,我们基于这个模型来简单说明下

内存屏障

内存屏障在Java中通过volatile关键字体现。volatile会在适当的地方添加下面四种内存屏障。

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

内存屏障只保证可见性,不保证时序性。也就是说内存屏障只是解决了线程A修改的内容能立刻被线程B读到。

Java中锁按性质分可以分悲观锁和乐观锁。悲观锁基于锁指令实现,乐观锁基于CAS实现。

通过monitorenter和monitorexit两个指令实现悲观锁,这两个指令之间的指令不得重排,且独占。假设线程A和线程B同时执行一段代码,线程A先通过monitorenter获取到了锁,那么在线程A执行monitorexit之前,线程B都只能等待。

CAS即CompareAndSet,Java通过自旋以及CPU层级的指令实现。具体可参考JUC实现。假设有一个变量c,初始值为3。线程A和线程B同时修改这个变量,A,B都同时获取到了变量c的值,A首先进行修改,将值改成了4。B尝试修改,但是发现c的值现在是4而不是3,所以进行自旋等待,然后重新执行修改操作,将4改成了5。

ThreadLocal

最后说下ThreadLocal。ThreadLocal即本地线程变量,也就是将公共的变量直接拿到线程内使用,其中的修改对外不影响。谈不上解决了「可见性」和「时序性」。只是保证了当前线程内的修改不影响其它线程,其它线程的修改也不影响当前线程。

java volatile既然不能绝对保证线程安全,那意义何在

保证你要的数据是那个时刻真实的数据。这个需要结合CPU缓存来说明,很多时候,你要的数据只是CPU缓存的数据,而内存中的数据已经发生变化了(特别是多核CPU的场景)。

它能保证访问时数据的一致性,但不能保证你处理过程中数据的一致性。

Java有哪些不好的设计

Java的出发点是提供一个比C/C++“安全”得多的编程环境。虽然GC和数组越界检查起到了很大的作用,但是Java又在以下3点偏离了安全初衷,使得程序员仍然需要时时刻刻提醒自己才不会犯错

  1. 整数计算会无声overflow/underflow。这就是说你不能用c = (a + b) / 2来计算两个整数的平均值。由于这个非常反直觉,而且一旦溢出程序员也得不到提示,因此历史上造成了JRE的标准库里潜伏很多很多年的bug
  2. 多线程情况下的Memory Model。由于向性能妥协,这个机制偏复杂,在没有深入研究的情况下大多数程序员都会犯错。更糟的是很多Java程序员甚至不知道这个概念(也许连volatile这个关键字都没用过)
  3. Exception。理论上Unchecked Exception几乎可以从任何一行调用中冒出来,因此分析代码的时候程序员不仅要理解正常情况下的程序流程,还要兼顾任何一行冒出异常的情况。这是非常大的思想负担(C程序员就不要担心这个)。如果处理不好,异常会破坏程序核心对象图的完整性,导致任意后果的程序bug