本篇内容介绍了“单线程和多线程中的可见性的区别是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
当一个线程修改了某个共享变量时(非局部变量,所有线程都可以访问得到),其他线程总是能立马读到最新值,这时我们就说这个变量是具有可见性的
如果是单线程,那么可见性是毋庸置疑的,肯定改了就能看到(直肠子,有啥说啥,大家都能看到)
但是如果是多线程,那么可见性就需要通过一些手段来维持了,比如加锁或者volatile修饰符(花花肠子,各种套路让人措手不及)
PS:实际上,没有真正的直肠子,据科学研究表明,人的肠子长达8米左右(~身高的5倍)
单线程和多线程中的可见性对比
volatile修饰符
指令重排序
volatile和加锁的区别
这里我们举两个例子来看下,来了解什么是可见性问题
下面是一个单线程的例子,其中有一个共享变量
public class SignleThreadVisibilityDemo { // 共享变量 private int number; public void setNumber(int number){ this.number = number; } public int getNumber(){ return this.number; } public static void main(String[] args) { SignleThreadVisibilityDemo demo = new SignleThreadVisibilityDemo(); System.out.println(demo.getNumber()); demo.setNumber(10); System.out.println(demo.getNumber()); } }
输出如下:可以看到,第一次共享变量number为初始值0,但是调用setNumber(10)之后,再读取就变成了10
0 10
改了就能看到,如果多线程也有这么简单,那多好(来自菜鸟的内心独白)。
下面我们看一个多线程的例子,还是那个共享变量
package com.jalon.concurrent.chapter3; /** * <p> * 可见性:多线程的可见性问题 * </p> * * @author: JavaLover * @time: 2021/4/27 */ public class MultiThreadVisibilityDemo { // 共享变量 private int number; public static void main(String[] args) throws InterruptedException { MultiThreadVisibilityDemo demo = new MultiThreadVisibilityDemo(); new Thread(()->{ // 这里我们做个假死循环,只有没给number赋值(初始化除外),就一直循环 while (0==demo.number); System.out.println(demo.number); }).start(); Thread.sleep(1000); // 168不是身高,只是个比较吉利的数字 demo.setNumber(168); } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } }
输出如下:
你没看错,就是输出为空,而且程序还在一直运行(没有试过,如果不关机,会不会有输出number的那一天)
这时就出现了可见性问题,即主线程改了共享变量number,而子线程却看不到
原因是什么呢?
我们用图来说话吧,会轻松点
步骤如下:
子线程读取number到自己的栈中,备份
主线程读取number,修改,写入,同步到内存
子线程此时没有意识到number的改变,还是读自己栈中的备份ready(可能是各种性能优化的原因)
那要怎么解决呢?
加锁或者volatile修饰符,这里我们加volatile
修改后的代码如下:
public class MultiThreadVisibilityDemo { // 共享变量,加了volatile修饰符,此时number不会备份到其他线程,只会存在共享的堆内存中 private volatile int number; public static void main(String[] args) throws InterruptedException { MultiThreadVisibilityDemo demo = new MultiThreadVisibilityDemo(); new Thread(()->{ while (0==demo.number); System.out.println(demo.number); }).start(); Thread.sleep(1000); // 168不是身高,只是个比较吉利的数字 demo.setNumber(168); } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } }
输出如下:
168
可以看到,跟我们预期的一样,子线程可以看到主线程做的修改
下面就让我们一起来探索volatile的小世界吧
volatile是一种比加锁稍弱的同步机制,它和加锁最大的区别就是,它不能保证原子性,但是它轻量啊
我们先把上面那个例子说完;
我们加了volatile修饰符后,子线程就可以看到主线程做的修改,那么volatile到底做了什么呢?
其实我们可以把volatile看做一个标志,如果虚拟机看到这个标志,就会认为被它修饰的变量是易变的,不稳定的,随时可能被某个线程修改;
此时虚拟机就不会对与这个变量相关的指令进行重排序(下面会讲到),而且还会将这个变量的改变实时通知到各个线程(可见性)
用图说话的话,就是下面这个样子:
可以看到,线程中的number备份都不需要了,每次需要number的时候,都直接去堆内存中读取,这样就保证了数据的可见性
指令重排序指的是,虚拟机有时候为了优化性能,会把某些指令的执行顺序进行调整,前提是指令的依赖关系不能被破坏(比如int a = 10; int b = a;此时就不会重排序)
下面我们看下可能会重排序的代码:
public class ReorderDemo { public static void main(String[] args) { int a = 1; int b = 2; int m = a + b; int c = 1; int d = 2; int n = c - d; } }
这里我们要了解一个底层知识,就是每一条语句的执行,在底层系统都是分好几步走的(比如第一步,第二步,第三步等等,这里我们就不涉及那些汇编知识了,大家感兴趣可以参考看下《实战Java高并发》1.5.4);
现在让我们回到上面这个例子,依赖关系如下:
可以看到,他们三三成堆,互不依赖,此时如果发生了重排序,那么就有可能排成下面这个样子
(上图只是从代码层面进行的效果演示,实际上指令的重排序比这个细节很多,这里主要了解重排序的思想先)
由于m=a+b需要依赖a和b的值,所以当指令执行到m=a+b的add环节时,如果b还没准备好,那么m=a+b就需要等待b,后面的指令也会等待;
但是如果重排序,把m=a+b放到后面,那么就可以利用add等待的这个空档期,去准备c和d;
这样就减少了等待时间,提升了性能(感觉有点像上学时候学的C,习惯性地先定义变量一大堆,然后再编写代码)
区别如下
加锁 | volatile | |
---|---|---|
原子性 | ✅ | ❎ |
可见性 | ✅ | ✅ |
有序性 | ✅ | ✅ |
上面所说的有序性指的就是禁止指令的重排序,从而使得多线程中不会出现乱序的问题;
我们可以看到,加锁和volatile最大的区别就是原子性;
主要是因为volatile只是针对某个变量进行修饰,所以就有点像原子变量的复合操作(虽然原子变量本身是原子操作,但是多个原子变量放到一起,就无法保证了)
“单线程和多线程中的可见性的区别是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。