Java面试之Volatile关键字(详解)

你好我是辰兮很高兴你能来阅读,本篇给你分析Java面试常考的Volatile 关键字,分享获取新知,希望对你有帮助,大家一起进步!



一、序言

面试常会遇到Volatile 关键字的相关问题

在这里插入图片描述
Volatile修饰符其实很早就存在于C和C++中
在这里插入图片描述
在了解Volatile关键字的时候,我们要了解Java的内存模型在这里插入图片描述

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的⼀种抽象规范,⽤来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到⼀致的内存访问效果。

ps:初学者很容易吧Java内存模型和Java内存结构搞混,注意这是两个不同的概念。

在这里插入图片描述
1.主内存(Main Memory)

主内存可以简单理解为计算机当中的内存,但⼜不完全等同。主内存被所有的线程所共享,对于⼀个共享变量(⽐如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。

2.工作内存(Working Memory)

工作内存可以简单理解为计算机当中的CPU⾼速缓存,但又不完全等同。每⼀个线程拥有自己的工作内存,对于⼀个共享变量来说,工作内存当中存储了它的“副本”。

线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。

在这里插入图片描述

你要知道如果全部在主内存中完成,操作效率会很低。
在这里插入图片描述

感兴趣的参考我的另外一篇文章:硬盘,内存和CPU的关系


二、volatile概念简介

1、volatile是Java提供的一种轻量级的同步机制。

2、Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

3、volatile关键字具有许多特性,其中最重要的特性就是保证了用volatile修饰的变量对所有线程的可见性。


小结:volatile是一种同步机制,相对synchronized更加轻量级,保证了可见性


拓展补充:这里的可见性是什么意思呢?

当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。

在这里插入图片描述


三、JMM以及共享变量的可见性

1、JMM决定一个线程对共享变量的写入何时对另一个线程可见

2、JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
在这里插入图片描述
小结:Java内存模型决定了共享变量的可见性,每一个线程操作数据都是在自己的内本地内存中操作,不能直接在主内存中操作。

想了解Java内存模型请参考:Java内存模型详解


四、并发编程的基本概念

(1)原子性

定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。

简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

Java中的原子性操作包括:

  • a. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
  • b.所有引用reference的赋值操作
  • c.java.concurrent.Atomic.* 包中所有类的一切操作

(2)可见性

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。

当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。


(3)有序性

定义:即程序执行的顺序按照代码的先后顺序执行。

Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。

Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。

另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。


五、锁的互斥和可见性

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。

(1)互斥即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。

(2)可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • a.对变量的写操作不依赖于当前值。
  • b.该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

在这里插入图片描述


六、volatile变量的特性

(1)保证可见性,不保证原子性

  • a.当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
  • b.这个写会操作会导致其他线程中的缓存无效。

(2)禁止指令重排

重排序:是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。

重排序需要遵守一定规则:

  • a.重排序操作不会对存在数据依赖关系的操作进行重排序。

比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。

  • b.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

解释一下重排序

编译器或者CPU的代码的结构重排排序,达到最佳效果。

1、编译器重排(案例)

//优化前
int X=1;
inty=2;
int a1=x*1;
int b1 = y*1;
inta2=X*2;
intb2=y*2;
//优化后
intX=1;
inty=2;
int a1=x*1;
inta2=x*2;
intb1=y*1;
intb2=y*2;

CPU只读一次的x和y值。不需反复读取寄存器来交替x和y值。


七、Volatile原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。

在这里插入图片描述
什么是内存屏障?

内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令

是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。


观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

① 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

②它会强制将对缓存的修改操作立即写入主存;

③如果是写操作,它会导致其他CPU中对应的缓存行无效。


The best investment is to invest in yourself

在这里插入图片描述

2020.07.07 记录辰兮的第87篇博客

猜你喜欢

转载自blog.csdn.net/weixin_45393094/article/details/106409860
今日推荐