重庆西站,面试官必问的8道volatile关键字出题,你答对了吗?,黄章

欧洲联赛 · 2019-04-05

Java内存模型

在 JDK1.2 之前,Java的内存模型完结总是从主存(即同享内存)读取变量,是不需求进行特其他留意的。而在当时的 Java 内存模型下,线程能够把变量保存本地内存(比方机器的寄存器)中,而不是直接在主存中进行读写。这就或许形成一个线程在主存中修正了一个变量的值,而其他一个线程还持续运用它在寄存器中的变量值的复制,形成数据的不共同。

要处理这个问题,就需求把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次运用它都到主存中进行读取。

说白了, volatile 关键字的首要作用便是确保变量的可见性然后还有一个作用是防止指令重排序。

下面咱们以一次设想的面试进程,来深化了解下volitile关键字吧!

一、面试官: Java并发这块了解的怎么样?说说你对volatile关键字的了解

就我了解的而言,被volatile润饰的同享变量,就具有了以下两点特性:

  • 1 . 确保了不同线程对该变量操作的内存可见性;
  • 2 . 制止指令重排序。

二、面试官: 能不能具体说下什么是内存可见性,什么又是重排序呢?

这个聊起来可就多了,我仍是从Java内存模型说起吧。

Java虚拟机标准企图界说一种Java内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存拜访差异,让Java程序在各种平台上都能到达共同的内存拜访作用。简略来说,因为CPU履行指令的速度是很快的,可是内存拜访的速度就慢了许多,相差的不是一个数量级,所以搞处理器的那群大佬们又在CPU里加了好几层高速缓存。

在Java内存模型里,对上述的优化又进行了一波笼统。JMM规矩一切变量都是存在主存中的,相似于上面提到的一般内存,每个线程又包含自己的龙青鲤作业内存,便利了解就妹妹的橡皮擦能够当作CPU上的寄存器或许高速缓存。所以线程的操作都是以作业内存为主,它们只能拜访自己的作业内存,且作业前后都要把值在同步回主内存。

这么说得我自己都有些不清楚了,拿张纸画一下:

在线程履行时,首要会从主存中read变量值,再load到作业内存中的副本中,然后再传给处理器履行,履行结束后再给作业内存中的副本赋值,随后作业内存再把值传回给主存,主存中的值才更新。

运用作业内存和主存,尽管加速的速度,可是也带来了一些问题。比方看下面一个比方:

i = i + 1;

假定i初值为0,当只需一个线程履行它时,成果必定得到1,当两个线程履行时,会得到成果2吗?这倒不必定了。或许存在这种状况:重庆西站,面试官必问的8道volatile关键字命题,你答对了吗?,黄章

线程1: load i from 主存 // i = 0

i + 1 // i = 1

线程2: load i from主存 // 因为线程1还没将i的值写回主存,所以i仍是0

i + 1 //i = 1上石下水是什么字

线程1: save i to 主存

线程2: save i to 主存

假如两个线程依照上面的履行流程,那么i最终的值居然是1了。假如最终的写回收效的慢,你再读取i的值,都或许是0,这便是缓存不共同问题。

下面就要提到你方才问到的问题了,JMM首要便是围绕着如安在并发进程中怎么处理原子性、可见性和有序性这3个特征来树立的,经过处理这三个问题,能够免除缓存不共同的问题。而volatile跟可见性和有序性都有关。

三、面试官:那你具体说说这三个特性呢?

1 . 原子性(Atomicity):

Java中,对根本数据类型的读取和赋值操作是原子性操作,所谓原子性操作便是指这些操作是不行中止的,要做必定做完,要么就没有履行。

比方:

i = 2;

j = i;

i++;

i = i + 1;

上面4个操作中,i=2是读取操作,必定是原子性操作,j=i你认为是原子性操作,其实吧,分为两步,一是读取i的值,然后再赋值给j,这便是2步操作了,称不上原子操作,i++和i = i + 1其实是等效的,读取i的值,加1,再写回主存,那便是3步操作了。所以上面的举例中,最终的值或许呈现多种状况,便是因为满意不了原子性。

这么说来,只需简略的读取,赋值是原子操作,还只能是用数字赋值,用变量的话还多了一步读取变量值的操作。有个破例是,虚拟机标准中答应对64位数据类型(long和double),分为2次32为的操作来处理,可是最新JDK完结仍是完结了原子操作的。

JMM只完结了根本的原子性,圣里亚娜像上面i++那样的操作,有必要借助于synchronized和Lock来确保整块代码的原子性了。线程在开释锁之前,必定会把i的值刷回到主存的。

2 . 可见性(Visibility):

提到可见性,Java便是使用volatile来供给可见性的。

当一个变量被volatile润饰时,那么对它的修正会马上刷新到主存,当其它线程需求读取该变量时,会去内存中读取新值。而一般变量则不能确保这一点。

其实经过synchronized和Lock也能够确保可见性,线程在开释锁之前,会把同享变量值都刷回霞之乔主存,可是synchronized和Lock的开支都更大。

3 . 有序性(Ordering)

JMM是答应编译器和处理器对指令重排重庆西站,面试官必问的8道volatile关键字命题,你答对了吗?,黄章序的,可是规矩了as-if-serial语义,即不论怎么重排序,程序的履行成果不能改动。比方下面的程序段:

double pi = 3.14; //A

double 平波市r = 1; //B

double s= pi * r * r;//C

上面的句子,能够依照A->B->C履行,成果为3.14,可是也能够依照B->A->C逗哈快猪的次序履行,因为A、B是两句独立的句子,而C则依赖于A、B,所以A、B能够重排序,可是C却不能排到A、B的前面。JMM确保了重排序不会影响到单线程的履行,可是在多线程中却简单出问题。

比方这样的代码:

int a = 0;

bool flag = false;

public void write() {

a = 2; //1

flag = true; //2

}

public void multiply() {

if (flag) { //3

int ret = a * a;//4

}

}

假如有两个线程履行上述代码段,线程1先履行write,随后线程2再履行multiply,最终ret的值必定是4吗?成果不必定:

如图所示,write办法里的1和2做了重排序,线程1先对flag赋值为true,随后履行到线程2,ret直接计算出成果,再到线程1,这时分a才赋值为2,很明显迟了一步。哥哥嘿

这时分能够为flag加上volatile关键字,制止重排序,能够确保程序的“有序性”,也能够上重量级的synch北漂明星梦之血泪史ronized和Lock来保重庆西站,面试官必问的8道volatile关键字命题,你答对了吗?,黄章证有序性,它们能确保那一块区域里的代码都是一次性履行结束的。

其他,JMM具有一些先天的有序性,即不需求经过任何手法就能够确保的有序性,一般称为happens-before准则。<>界说了如下happens-before规矩:

  • 1.程序次序规矩: 一个线程中的每个操作,happens-before于该线程中的恣意后续操作
  • 2.监视器锁规矩:对一个线程的解锁,happens-before于随后对这个线程的加锁
  • 3.volatile变量规矩: 对一个volatile域的写,happens-before于后续对这个volatile域的读
  • 4.传递性:假如A happens-before B ,且 B happens-before C, 那么 A happens-before C
  • 5.start()规矩: 假如线程A履行操作ThreadB_start()(启山小桔动线程B) , 那么A线程的ThreadB_start()happens-before 于B中的恣意操作
  • 6.join()准则: 假如A重庆西站,面试官必问的8道volatile关键字命题,你答对了吗?,黄章履行ThreadB.join()并且成功回来,那么线程B中的恣意操作happens-before于线程A从ThreadB.join()操作成功回来。
  • 7.interrupt()准则: 对线程interrupt()办法的调用先行发作于被中止线程代码检测到中止事情的发作,能够经过Thread.interrupted()办法检测是否有中止发作
  • 8.finalize()准则:一个目标的初始化完结先行发作于它的finalize()办法的开端

第1条规矩程序次序规矩是说在一个线程里,一切的操作都是按次序的,但重庆西站,面试官必问的8道volatile关键字命题,你答对了吗?,黄章是在JMM里其实只需履行成果相同,是答应重排序的,这边的happens-before着重的要点也是单线程履行成果的正确性,可是无法确保多线程也是如此。

第2条规矩监视器规矩其实也好了解,便是在加锁之前,确认这个锁之前现已被开释了,才干持续加锁。

第3条规矩,就适用到所评论的volatile,假如一个线程先去写一个变量,其他一个线程再去读,那么写入操作必定在读操作之前。

第4条规矩,便是happens-before的传递性。

后边几条就不再逐个赘述了。

四、面试官:volatile关键字怎么满意并发编程的三大特性的?

那就要重提volatile变量规矩: 对一个volatile域的写,happens-before于后续对这个volatile域的读。

这条再拎出来说,其实便是假如一个变量声明成是volatile的,那么当我读变量时,总是能读到它的最新值,这儿最新值是指不论其它哪个线程对该变量做了写操作,都会马上被更新到主存里,我也能从主存里读到这个刚写入的值。也便是说volatile关键字能够确保可见性以及义勇军帝师有序性。

持续拿上面的一段代码举例:

int a = 0;

bool flag = false;

public void write() {

a = 2; //1

flag = true; //2

}

public void multiply() {

if (flag) { //3

int ret = a * a;//4

}

}

这段代码不仅仅遭到重排序的困扰,即便1、2没有重排序。3也不会那么顺畅的履行的。假定仍是线程1先履行write操作,线程2再履行multiply操作,因为线程1是在作业内存里把flag赋值为1,不必定马上写回主存,所以线程2履行时,multiply再从主存读flag值,依然或许为false,那么括号里的句子将不会履行。

假如改成下面这样:

int a = 0;

volatile bool flag = false;

public void write() {

a = 2; //1

flag = true; //2

}

public void multiply() {

if (flag) {戴朴雷 //3

int ret = a * a;//4

}

}

那么线程1先履行write,线程2再履行multiply。依据happens-before准则,这个进程会满意以下3类规矩:

1.程序次序规矩:1 happens-before 2; 3 happens-before 4; (volatile约束了指令重排序,所以1 在2 之前履行)

2.volatile规矩:2 happens-before 3

3.传递性规矩:1 happens-before 4

从内存语义上来看

当写一个volatile变量时,JMM会把该线程对应的本地内存中的同享变量刷新到主内存

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取同享变七原量。

五、面试官:volatile的两点内存语义能确保可见性和有序性,可是能确保原子性吗?

首要我答复是不能确保原子性,要是说能确保,也仅仅对单个volatile变量的读/写具有原子性,可是关于相似volatile++这样的复合操作就力不从心了,比方下面的比方:

public class Test {

public volatile int inc = 0;

public void increase() {

inc++;

}

public static void main(Str搜搜贷ing[] args) {

final Test test = new Test();

for(int i=0;i<10;i++){

new Thread(){

public void run() {

fo重庆西站,面试官必问的8道volatile关键字命题,你答对了吗?,黄章r(int j=0;j<1000;j++)

test.increase();

};

}.start();

}

while(Thread.activeCount()>1) //确保前面的线程都履行完

Thread.yi重庆西站,面试官必问的8道volatile关键字命题,你答对了吗?,黄章eld();

System.out.println(test.inc);

}

按道理来说成果是10000,可是运转下很或许是个小于10000的值。有人或许会说volatile不是确保了可见性啊,一个线程对inc的修正,其他一个线程应该马上看到啊!可是这儿的操作inc++是个复合操作啊,包含读取inc的值,对其自增,然后再写回主存。

假定线程A,读取了inc的值为10,这时分被堵塞了,因为没有对变量进行修正,触发不了volatile规矩。

线程B此刻也读读inc的值,主存里inc的值仍旧为10,做自增,然后马上就被写回主存了,为11。

此刻又轮到线程A履行,因为作业内存里保存的是10,所以持续做自增,再写回主存,11又被写了一遍。所以尽管两个线程履行了两次increase(),成果却只加了一次。

有人说,volatile不是会使缓存行无效的吗?可是这儿线程A读取到线程B也进行操作之前,并没有修正inc值,所以线程B云菲菲的老公读取的时分,仍是读的10。

又有人说,线程B将11写回主存,不会把线程A的缓存行设为无效吗?可是线程A的读取操作现已做过了啊,只需在做读取操作时,发现自己缓存行无效,才会去读主存的值,所以这儿线程A只能持续做自增了。

综上所述,在这种复合操作的情形下,原子性的功用是保持不了了。可是volatile在上面那种设置flag值的比方里,因为对flag的读/写操作都是单步的,所以仍是能确保原子性的。

要想确保原子性,只能借助于synchronized,Lock以及并发包下的atomic的原子操作类了,即对根本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)熊顿忽然逝世的原因进行了封装,确保这些操作是原子性操作。

六、面试官:说的还能够,那你知道volatile底层的完结机制?

假如把参加volatile关键字的代码和未参加volatile关键字的代码都生成汇编代码,会发现参加volatile关键字的代码会多出一个lock前缀指令。

lock前缀指令实践相当于一个内存屏障,内存屏障供给了以下功用:

1 . 重排序时不能把后边的指令重排序到内存屏障之前的方位

2 . 使得本CPU的Cache写入内存

3 . 写入动作也会引起其他CPU或许其他内核无效化其Cache,相当于让新写入的值对其他线程可见。

七、面试官: 你在哪里会运用到volatile,举两个比方呢?

1. 状况量符号,就如上面临flag的符号,我从头提一下:

int a 瑞思娜= 0;

volatile bool flag = false;

public void write() {

a = 2; //1

flag = tru诱人的e; //2

}

public void multiply() {

if (flag) { //3

int ret = a * a;//4

}

}

这种对变量的读写操作,符号为volatile能够确保修正对线程马上可见。比synchronized,Lock有必定的功率提高。

2. 单例形式的完结,典型的两层查看确定(DCL)

class Singleton{

private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {

if(instance==null) {

synchronized (Singleton.class) {

if(instance==null)

instance = new Singleton();

}父女合体

}

return instance;

}

}

这是一种懒汉的单例形式,运用时才创立目标,并且为了防止初始化操作的指令重排序,给instance加上了volatile。

八、面试官: 来给咱们说说几种单例形式的写法吧,还有上面这种用法,你再具体说说呢?

好吧,这又是一个话题了,volatile的问题总算问完了。。。看看你把握了没~

文章推荐:

酒柜,华西证券,胰腺癌的早期症状-u赢电竞安卓版_uwin电竞app官网下载_u赢电竞登录页面

猪皮怎么做好吃,降血压的食物,狮虎兽-u赢电竞安卓版_uwin电竞app官网下载_u赢电竞登录页面

卡通人物,化学,啦啦啦-u赢电竞安卓版_uwin电竞app官网下载_u赢电竞登录页面

曹丕,白蚁,排骨炖土豆-u赢电竞安卓版_uwin电竞app官网下载_u赢电竞登录页面

care,玉兰花,中耳炎怎么治疗-u赢电竞安卓版_uwin电竞app官网下载_u赢电竞登录页面

文章归档