【面试篇】Java多线程并发-Java关键字volatile详解
发布日期:2021-06-29 15:34:25 浏览次数:2 分类:技术文章

本文共 4605 字,大约阅读时间需要 15 分钟。

目录:

【Java锁体系】九、volatile关键字

volatile关键字在需要保证多线程共享变量的可见性时,用volatile来修饰变量。

volatile关键字很适合只有一个线程修改,其它线程读取的情况。volatile变量被修改之后,对其它线程立即可见。

9.1 synchronized和volatile关键字的单例模式例子

补充:单例模式定义-一个类有且仅有一个实例,并且自行实例化向整个系统提供。

其实现方式主要是通过饿汉式(线程安全的)和懒汉式(线程不安全的)。

饿汉式

package com.lcz.syn;// 单例模式实现方式-饿汉式class SingleOne{
// 创建类中私有构造 private SingleOne(){
} // 创建私有对象 private static SingleOne instance = new SingleOne(); // 创建公有静态返回 public static SingleOne getInstance(){
return instance; }}public class Test1 {
// 主函数 public static void main(String[] args){
SingleOne singleOne1 = SingleOne.getInstance(); SingleOne singleOne2 = SingleOne.getInstance(); System.out.println(singleOne1 == singleOne2); }}

懒汉式

// 单例模式实现方式-懒汉式class SingeleTwo{
// 创建类中私有构造 private SingeleTwo(){
} // 创建静态私有对象 private static SingeleTwo instance; // 创建返回 public static SingeleTwo getInstance(){
if (instance==null){
instance = new SingeleTwo(); } return instance; }}public class Test1 {
// 主函数 public static void main(String[] args){
SingeleTwo singleOne1 = SingeleTwo.getInstance(); SingeleTwo singleOne2 = SingeleTwo.getInstance(); System.out.println(singleOne1 == singleOne2); }}

但是懒汉式在多线程下不安全。

这里通过双重校验锁使其安全。

双重校验锁实现懒汉式

// 双重校验锁实现class Singleton{
//私有化 private Singleton(){
} // 私有化对象 private volatile static Singleton instance; // 返回方法 public static Singleton getInstance(){
if (instance==null){
// 加锁 synchronized (Singleton.class){
if (instance==null){
instance = new Singleton(); } } } return instance; }}

1.为什么使用volatile修饰了singleton引用还用synchronized?

volatile 只保证了共享变量 singleton 的可见性,但是 singleton = new Singleton(); 这个操作不是原子的,可以分为三步:

步骤1:在堆内存申请一块内存空间;

步骤2:初始化申请好的内存空间;

步骤3:将内存空间的地址赋值给 singleton;

所以singleton = new Singleton(); 是一个由三步操作组成的复合操作,多线程环境下A 线程执行了第一步、第二步之后发生线程切换,B 线程开始执行第一步、第二步、第三步(因为A 线程singleton 是还没有赋值的),所以为了保障这三个步骤不可中断,可以使用synchronized 在这段代码块上加锁。

2.第一次检查singleton为空后为什么内部还进行第二次检查

A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 可能会初始化二次。

3.volatile 除了内存可见性,还有别的作用吗?

volatile 修饰的变量除了可见性,还能防止指令重排序。

指令重排序 是编译器和处理器为了优化程序执行的性能而对指令序列进行重排的一种手段。现象就是CPU 执行指令的顺序可能和程序代码的顺序不一致,例如 a = 1; b = 2; 可能 CPU 先执行b=2; 后执行a=1;

singleton = new Singleton(); 由三步操作组合而成,如果不使用volatile 修饰,可能发生指令重排序。步骤3 在步骤2 之前执行,singleton 引用的是还没有被初始化的内存空间,别的线程调用单例的方法就会引发未被初始化的错误。

volatile关键字底层原理是通过内存屏障技术实现的

9.2 CPU缓存模型

CPU缓存是为了解决CPU处理速度和内存处理速度不对等的问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lqr2jHcr-1608212523505)(D:\software\typora\workplace\imgs_volatile\1.png)]

先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。

而针对缓存不一致性的问题可以通过执行缓存一致性协议或者其他手段来解决。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tWmJwlGX-1608212523506)(D:\software\typora\workplace\imgs_volatile\2.png)]

但还存在一个问题,每个处理器上的store buffer(写缓冲区),仅仅对它所在的处理器可见。即一个处理器的写缓冲器中的内容是无法被其它处理器读取的,这个也就造成了处理器更新一个共享变量后,对其它处理器而言,看不到这个更新的值,即可见性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dPz7md8g-1608212523508)(D:\software\typora\workplace\imgs_volatile\3.png)]

9.3 内存屏障

为了解决写缓冲器和无效化队列带来的可见性和重排序问题,硬件设计者推出了新的方案:内存屏障。

内存屏障是被插入两个CPU指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障有序性的。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将写缓冲器的值写入高速缓存,清空无效队列,从而“附带”的保障了可见性。

举个例子说明:

Store1 Store2 Load1 StoreLoad屏障 Store3 Load2 Load3

对于上面的一组CPU指令(Store表示写入指令,Load表示读取指令),StoreLoad屏障之前的Store指令无法与StoreLoad屏障之后的Load指令进行交换位置,即重排序。但是StoreLoad屏障之前和之后的指令是可以互换位置的,即Store1可以和Store2互换,Load2可以和Load3互换。

StoreLoad屏障的目的在于使屏障前的写操作的结果,对于屏障后的读操作是可见的。为了保障这一点,除了指令不能重排序外,StoreLoad屏障还会在写操作完之后,将写缓冲器中的条目冲刷入高速缓存或主内存;在读操作之前,清空无效化队列,从主内存或其他处理器的高速缓存中读取最新值到自己的内存。从而保障了数据在不同处理器之间是一致的,即可见性。

9.4 JMM(Java内存模型)

JMM 是Java 定义的一套协议,用来屏蔽各种硬件和操作系统的内存访问差异,让Java 程序在各种平台都能有一致的运行效果。

所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量(主内存的拷贝),线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rb1xc06g-1608212523510)(D:\software\typora\workplace\imgs_volatile\4.png)]

线程本地内存和物理真实内存之间的关系

  • 初始变量首先存储在主内存中;
  • 线程操作变量需要从主内存拷贝到线程本地内存中;
  • 线程的本地工作内存是一个抽象概念,包括了缓存、store buffer(后面会讲到)、寄存器等

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sLra8LU6-1608212523512)(D:\software\typora\workplace\imgs_volatile\5.png)]

多线程如何通过共享变量通信

1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2)线程B到主内存中去读取线程A之前已更新过的共享变量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6FyOJ4tI-1608212523513)(D:\software\typora\workplace\imgs_volatile\6.png)]

9.5 并发编程的三个重要特性

  • **原子性:**一个的操作或一组操作要么全部执行,要么全部不执行。synchronized保证代码片段的原子性。
  • **可见性:**当多个线程共同访问同一个共享变量时,一个线程修改了这个变量的值,其它线程能够立即看到修改后的值。volatile关键字可以保证共享变量的可见性。
  • **有序性:**代码在执行的过程中的先后顺序,Java在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时的顺序。volatile关键字可以禁止指令进行重排序优化。

9.6 synchronized关键字和volatile关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • synchronized本质上是一种阻塞锁;而volatile则是使用了内存屏障来实现的;
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

转载地址:https://codingchaozhang.blog.csdn.net/article/details/110631110 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:【面试篇】Java多线程并发-ThreadLocal关键字详解
下一篇:【Leetcode刷题篇】leetcode121买卖股票的最佳时机

发表评论

最新留言

关注你微信了!
[***.104.42.241]2024年04月27日 05时13分04秒