目录
1. 面试官:“请谈谈你对volatile的理解”
2. 面试官接着问:“既然volatile不能保证原子性,那工作中如何解决这个问题呢?”
3. 官试官:“volatile是如何保证多线程环境下的可见性,JAVA内存模型JMM谈谈你对它的了解”
4. 官试官:“你能编写一个volatile修饰的变量在多线程环境可见性示例不?”
5. 面试官:“为什么volatile不保证原子性呀,你知道底层原理么?”
6. 面试官:“你可以编写一下,volatile不保证原子性的解决示例?”
7. 面试官:volatile能禁止指令重排 ,为什么有指令重排,volatile中怎样做到禁止指令重排?
8. 面试官:你在哪些地方用到过volatile?手写一个Volatile单例
小李子窃窃私语,这不是正好前段时间复习且项目中使用到的volatile关键字么?于是小李子自信地答道:
volatile是Java虚拟机提供的轻量级的同步机制,volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而进行指令重排序,同时保证多线程环境下变量值变修改后,其他线程可见性;但volatile不保证原子性哦。使用示例:
volatile int number = 0; // 使用volatile 修饰整型变量number,以保证多线程环境下可见性及禁止指令重排。注:volatile不保证原子性
小李子心想,还来了个连环炮,好在我有准备。于是比利比利地回答:
工作中,我们有两种方式规避这个问题:
1. 使用JDK提供的 Atomic原子类
比如:AtomicInteger来声明变量,是采用了CAS 比较并交换 compareAndSwapInt,底层调用的是native方法,其意思是通过hotspot底层c/c++方法实现。最终实现是调用了 cmpxchg,cmpxchg指令在多线程下也是有可能被打断,所以在加入lock指令 不允许其他线程访问这块内存区域的数据。
2. 加锁,如synchronized 或 ReentrantLock
小李子心底阵阵发凉,前面已经回答的很好,还接着问,有完没完!没办法,回忆一下,继续答道:
Java内存模型(Java Memory Model,简称JMM) 本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM关于同步规定:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作 (读取赋值等) 必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,线程间的通讯(传值) 必须通过主内存来完成。
这样子吧,我画一个JMM内存模型图出来看看就清楚了。JMM内存模型图如下:
小李子听到这个就想要骂娘了,还得编一个示例。不过,小Case,小李子接过键盘,啪啦啪啦地敲下代码:
package com.java.meet.c01_11_volatile;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
class MyResource {
//int number = 0; // 此行代码在多线程环境下,变量修改后不可见
volatile int number = 0; // 此行代码在多线程环境下,变量修改后可见,因为变量被 volatile修饰
public void addT060(){
this.number = 60;
}
}
public class C_02_Volatile_SeeOK {
public static void main(String[] args) { // main是一切方法的运行入口
seeOkByVolatile();
}
// volatile 可以保证可见性,及时通知其它线程,主物理内存的值已经被修改。
// volatile 是通过内存屏障实现
private static void seeOkByVolatile() {
MyResource res = new MyResource(); // 资源类
new Thread(() -> {
System.out.println(LocalDateTime.now() + "\t" + Thread.currentThread().getName() + "线程\t当前值为:" + res.number);
// 暂停一会儿线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e){ e.printStackTrace();}
res.addT060();
System.out.println(LocalDateTime.now() + "\t" + Thread.currentThread().getName() + "线程\t当前值为:" + res.number);
}, "AAAA").start();
// 第2个线程就是我们的main线程
while(res.number == 0){
// main线程就一直在这里等待循环,走到number值不再等于零
}
System.out.println(LocalDateTime.now() + "\t" + Thread.currentThread().getName() + "线程\t当前值为" + res.number);
}
}
没有被volatile修饰变量,在多线程中被修改值,测试结果如下:
被volatile修饰变量,在多线程中被修改值,测试结果如下:
敲出来代码了,哈哈,小李子心里哈哈的暗自高兴~
小李子,捋了下思路,这个So easy嘛。说道:number++在多线程下是非线程安全的。当我们添加volatile 关键字修饰时,n++ 同样被拆分成了3个指令,在多线程环境下,依然不能保证原子性。如下图,是volatile 修饰整型变量n,对n++方法进行反汇编。
package com.java.meet.c01_11_volatile;
/**
* 6_volatile不保证原子理论解释
*
* 使用javap -c或javap -verbose
*/
public class C_06_T1 {
volatile int n = 0;
/**
* MyData.java ====> MyData.class ===> 字节码
* n++ 被拆分成了3个指令
* 执行getfield拿到原始n;
* 执行iadd进行加1操作;
* 执行putfield写把累加后的值写回
*
* 查看字节码,需要配置好External tools
*/
/**
/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/bin/javap -c com.java.meet.c01_11_volatile.C_06_T1
Compiled from "C_06_T1.java"
public class com.java.meet.c01_11_volatile.C_06_T1 {
volatile int n;
public com.java.meet.c01_11_volatile.C_06_T1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field n:I
9: return
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field n:I
5: iconst_1
6: iadd
7: putfield #2 // Field n:I
10: return
}
Process finished with exit code 0
*/
public void add(){
n++;
}
}
小李子胸有成竹的说:这样吧,我演示一个volatile修饰的变量在多线程环境下不保证原子性的及其中一个解决volatile不保证原子性办法。其中,可以通过JDK提供的Atomic原子类来保证原子操作,于是,拿起键盘哒哒地敲起来:
package com.java.meet.c01_11_volatile;
import java.util.concurrent.atomic.AtomicInteger;
class MyNumber {
volatile int number = 0;
// 请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性
public void addPlusPlus(){
number++;
}
// 使用JDK提供的原子类,可以解决volatile不保证原子性问题,采用CAS比较并交换
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
public class C_03_Volatile_NotSupportAtomic {
public static void main(String[] args) { // main是一切方法的运行入口
notSupportAtomicByVolatile();
}
// volatile 不保证原子性 及 使用JDK Atomic原子类解决volatile不保证原子性问题
private static void notSupportAtomicByVolatile() {
MyNumber res = new MyNumber();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
res.addPlusPlus();
res.addMyAtomic();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?
while (Thread.activeCount() > 2){
Thread.yield(); // yield我不执行,让其他的线程更好地执行
}
System.out.println(Thread.currentThread().getName() + "\t int type,finally number value:" + res.number);
System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type,finally number value:" + res.atomicInteger);
}
}
volatile不保证原子性测试结果如下:
小李子,心里苦呀,我只是想拿月薪30K的薪资,还得挖那么深,没有办法。继续回答:
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3种
单线程环境里面确保程序是最终执行结果和代码顺序执行的结果一致。处理器在进行重新排序是必须要考虑指令之间的数据依赖性。
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测。
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
由于编译器和处理器都能执行指令重排优化。如果在指令插入一条Memory B 和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
小李子喜出望外,这不是送分题。通过DCL双端检查 + Volatile,来实现,卡拉卡拉地写下如下代码:
package com.java.meet.c01_11_volatile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 单例模式:volatile版本
*
*/
public class C_10_SingletonDemo {
/**
* 多线程环境下,单例模式下,需要DCL机制 + volatile禁止指令重排
*/
//private static C_10_SingletonDemo instance = null; // 在非常高的并发情况下,可能获取的对象不是同一个
private static volatile C_10_SingletonDemo instance = null; // 在非常高的并发情况下,DCL双端检查 + volatile 可以保证获取的是同一个对象
// 单例类构造方法私有化
private C_10_SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t我是构造方法SingletonDemo()");
}
// 如果单纯依靠 DCL (Double Check Lock双端检锁机制),还是有问题,可能运行1000万次,才出一次问题
/**
* DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
*
* 原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
*
* instance=new SingletonDem(); 可以分为以下步骤(伪代码)
*
* memory=allocate(); //1.分配对象内存空间
*
* instance(memory); //2.初始化对象
*
* instance=memory; //3.设置instance的指向刚分配的内存地址,此时instance!=null
*
* 步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.
*
* memory=allocate(); //1.分配对象内存空间
*
* instance=memory; //3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
*/
public static C_10_SingletonDemo getInstance(){
if (instance == null){
synchronized (C_10_SingletonDemo.class){
if (instance == null){
instance = new C_10_SingletonDemo();
}
}
}
if (instance == null) {
System.out.println("----");
}
return instance;
}
// List本身是不安全的, 使用以下才安全
public static List list = Collections.synchronizedList(new ArrayList());
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(C_10_SingletonDemo.getInstance());
}, String.valueOf(i)).start();
}
// 等待上述操作完成
TimeUnit.SECONDS.sleep(2);
boolean oneSingleObject = true;
for (int i = 0; i < list.size(); i++) {
for (int j = i+1; j < list.size(); j++) {
if (list.get(i) != list.get(j)) {
System.out.println("此单例,创建了不同的对象实例!!!\t" + i + " "+ list.get(i) + "\t" + list.get(j));
oneSingleObject = false;
}
}
}
if (oneSingleObject) {
System.out.println("通过单例类获取:" + list.size() + " 次依然是同一个对象!");
}
}
}
DCL双端检查 + Volatile 单例测试结果如下:
面试官,看来你对volatile掌握得还不错哈~
今天面试,先到这里,回去等通过吧。
文章最后,给大家推荐一些受欢迎的技术博客链接:
欢迎扫描下方的二维码或 搜索 公众号“10点进修”,我们会有更多、且及时的资料推送给您,欢迎多多交流!