【多线程】之volatile关键字与内存可见性问题

在这里插入图片描述

大家好呀
我是浪前

今天给大家讲解的是**volatile关键字与内存可见性**
祝愿所有点赞关注的人,身体健康,一夜暴富,升职加薪迎娶白富美!!!
点我领取迎娶白富美大礼包

volatile关键字

作用:

  1. 保证内存可见性
  2. 禁止指令重排序

那么什么是内存可见性呢??

内存可见性

这里的内存可见性也是导致线程安全问题的原因之一:
在这里插入图片描述

但是在说明内存可见性之前,我们先来讲一讲CPU读取速度比较:

CPU读取速度排行

众所周知: 我们的计算机代码和程序经常需要去访问数据

而这些数据一般都是在内存中
比如我们定义了一个变量

那么这个变量就是存储在内存上的
因为这个变量是内存中的数据

所以我们的CPU在使用这个变量之前

就要先把这个变量(内存中的数据)
先读取出来放到CPU的寄存器中,然后才能够在寄存器中参与运算(load),即CPU要先读取内存,才能够进行操作

那么CPU是读取内存更快,还是读取硬盘更快呢?
小编画了一个排行榜,大家都凑合看看叭~

读取速度排行榜:

谁的读取速度更快谁就排第一

读寄存器>读内存>读硬盘

在这里插入图片描述

由图可知:CPU进行大部分操作都很快,但是一旦涉及到读/写内存,那么此时速度就慢下来了

所以CPU读取内存这个操作其实是很慢的,

那么如何去解决这个问题呢?
既然读取内存速度慢,那我们就尽量不去读取内存的操作就可以啦

此时为了提高效率,编译器会对代码进行优化

把一些本来要读取内存的操作优化成了读取寄存器的操作
减少了读内存的次数,也就可以提高整体的程序的效率了

注意上面的重点:
编译器为了提高效率会将一部分的读取内存操作优化为读取寄存器操作

通俗来说就是:编译器优化时会将读取内存的操作变成读取寄存器的操作

有了以上的背景知识之后,我们再来看这个内存可见性

线程安全问题

下面的代码就是由于内存可见性所产生的线程安全问题

public class Text{
	//创建一个isQuit变量
	private static int isQuit = 0;
	public static void main(String[] args){
		//t1线程读取isQuit,若isQuit为0,则一直运行不会结束,如果不为0,则t1线程终止
		Thread t1  = new Thread(() ->{
			while(isQuit == 0){
			}
			System.out.println("t1线程结束");
		});
		t1.start();

		//t2线程负责输入isQuit,输入结束之后,t2线程结束
		Thread t2 = new Thread(() ->{
			Scanner sc = new Scanner(System.in);
			System.out.println("请输入isQuit的值");
			isQuit = sc.nextInt();
		});
		t2.start();
	}

}

代码解读:
写了两个线程,t1线程读取isQuit这个变量

若isQuit为0, 则t1线程会一直运行,不会结束
如果isQuit不为0,则t1线程会终止

//t1线程读取isQuit,若isQuit为0,则一直运行不会结束,如果不为0,则t1线程终止
		Thread t1  = new Thread(() ->{
			while(isQuit == 0){
			}
			System.out.println("t1线程结束");
		});
		t1.start();

t2线程负责让用户输入isQuit的值,用户输入结束之后,t2线程就结束了

//t2线程负责输入isQuit,输入结束之后,t2线程结束
		Thread t2 = new Thread(() ->{
			Scanner sc = new Scanner(System.in);
			System.out.println("请输入isQuit的值");
			isQuit = sc.nextInt();
		});
		t2.start();

按照理想预期结果,当我们对isQuit输入一个非零的元素时,t1线程就会终止
但是实际结果却不会,
当我们输入3时,代码没有结束,即t1线程并没有结束…

如图所示:
在这里插入图片描述

这里由于多线程引起的bug,也是线程安全问题

问题深度剖析

这个线程安全问题就是内存可见性引起的:

//t1线程读取isQuit,若isQuit为0,则一直运行不会结束,如果不为0,则t1线程终止
		Thread t1  = new Thread(() ->{
			while(isQuit == 0){
				
			}
			System.out.println("t1线程结束");
		});
		t1.start();

t1线程中的while循环执行的步骤:

  1. load读取内存中的isQuit的值到寄存器中
  2. 通过cmp指令比较寄存器的值是否是0,决定是否要继续循环

由于循环很快,在短时间内就会进行大量的循环操作
就是进行大量的load和cmp操作

此时编译器/JVM 发现虽然进行了很多次的load,但是load出来的isQuit的值没有变化,都是一样的
并且load操作非常耗费时间,一次load的时间和执行上万次cmp的时间一样

于是编译器就进行了优化:在执行刚刚的循环代码的时候,
编译器只是在第一次循环的时候读取了内存(load了一次)
之后就都不再读取内存了
而是直接从寄存器中取出isQuit的值

编译器优化

编译器希望可以提高效率,但是提高效率的前提是保证逻辑不变,不会出现bug

此时由于是另一个线程(t2线程)对isQuit进行了修改
但是在t1线程中,编译器以为没人修改isQuit,于是就做出了优化,即只读取了一次内存,导致代码出现了bug

即使后续t2线程对isQuit已经进行了修改,但是由于t1线程只是在第一次时读取了内存
t1线程后续都没有再次读取内存了,此时的t1线程就不知道isQuit已经进行了修改:
即isQuit已经被改变了,但是t1线程一直都被蒙在鼓里不知道,

上述由于编译器优化出现了错误导致的线程安全问题就是内存可见性

解决方法

此时我们的volatile关键字就是解决内存可见性的方法

volatile的作用:

在多线程的环境下,编译器对于是否要进行优化的情况
是有可能会判定错误的

此时就需要使用volatile关键字来告诉编译器此处的代码不要进行优化

虽然此时失去了编译器的优化会导致效率变低,但是保证了代码正确

使用方法

给isQuit这个变量加上volatile关键字修饰,此时编译器就会自然禁止优化

代码如下:

public volatile static int isQuit = 0;

加上了volatile关键字之后,每次循环都会去读取内存上的isQuit的值
也就不会出现内存可见性的问题了

执行结果如图所示:
在这里插入图片描述

题外话

下面的代码中不加volatile关键字也不会触发编译器的优化。
比如:
我们在t1线程的循环中加入一个sleep(10),
此时也不会进行优化:代码也可以正常运行:

Thread t1  = new Thread(() ->{  
    while(isQuit == 0){  
        try {  
            Thread.sleep(10);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
    System.out.println("t1线程结束");  
});  
t1.start();

如图所示:
在这里插入图片描述

编译器到底什么时候对编译器进行优化,什么时候不优化,我们都不知道
所以最好还是加上一个volatile关键字更好

volatile不保证原子性

虽然这个volatile关键字可以解决内存可见性的问题,但是不能够保证原子性的
synchronized关键字可以保证原子性还可以保证内存可见性

因为Synchronized 加锁和解锁会触发主内存与工作内存的数据同步

  1. 进入临界区(加锁)

    • 线程会先把主内存中最新的共享变量值刷新到自己的工作内存中。
    • 确保临界区里的代码用的是最新的数据。
  2. 退出临界区(解锁)

    • 线程会把工作内存中修改后的共享变量值刷新回主内存。
    • 确保其他线程能看到修改后的值。

与synchronized的比较

特性synchronizedvolatile
内存可见性
原子性是(可以保证原子操作)否(仅保证可见性,不保证操作原子性)
作用范围修饰代码块或方法修饰变量
性能开销较高(需要获取和释放锁)较低

文章结束,感谢翻阅,希望小编可以和大家一起加油!!!
下一篇会讲述wait和notify~

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值