作者:汤圆

个人博客:javalover.cc

前言

官人们好啊,我是汤圆,今天给大家带来的是《对象的可见性 - volatile篇》,希望有所帮助,谢谢

文章如果有误,希望大家可以指出,真心感谢

简介

当一个线程修改了某个共享变量时(非局部变量,所有线程都可以访问得到),其他线程总是能立马读到最新值,这时我们就说这个变量是具有可见性的

如果是单线程,那么可见性是毋庸置疑的,肯定改了就能看到(直肠子,有啥说啥,大家都能看到)

但是如果是多线程,那么可见性就需要通过一些手段来维持了,比如加锁或者volatile修饰符(花花肠子,各种套路让人措手不及)

PS:实际上,没有真正的直肠子,据科学研究表明,人的肠子长达8米左右(~身高的5倍)

目录

  1. 单线程和多线程中的可见性对比
  2. volatile修饰符
  3. 指令重排序
  4. volatile和加锁的区别

正文

1. 单线程和多线程中的可见性对比

这里我们举两个例子来看下,来了解什么是可见性问题

下面是一个单线程的例子,其中有一个共享变量

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,而子线程却看不到

原因是什么呢?

我们用图来说话吧,会轻松点

步骤如下:

  1. 子线程读取number到自己的栈中,备份
  2. 主线程读取number,修改,写入,同步到内存
  3. 子线程此时没有意识到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的小世界吧

2. volatile修饰符

volatile是一种比加锁稍弱的同步机制,它和加锁最大的区别就是,它不能保证原子性,但是它轻量啊

我们先把上面那个例子说完;

我们加了volatile修饰符后,子线程就可以看到主线程做的修改,那么volatile到底做了什么呢?

其实我们可以把volatile看做一个标志,如果虚拟机看到这个标志,就会认为被它修饰的变量是易变的,不稳定的,随时可能被某个线程修改;

此时虚拟机就不会对与这个变量相关的指令进行重排序(下面会讲到),而且还会将这个变量的改变实时通知到各个线程(可见性)

用图说话的话,就是下面这个样子:

可以看到,线程中的number备份都不需要了,每次需要number的时候,都直接去堆内存中读取,这样就保证了数据的可见性

3. 指令重排序

指令重排序指的是,虚拟机有时候为了优化性能,会把某些指令的执行顺序进行调整,前提是指令的依赖关系不能被破坏(比如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,习惯性地先定义变量一大堆,然后再编写代码)

4. volatile和加锁的区别

区别如下

加锁 volatile
原子性
可见性
有序性

上面所说的有序性指的就是禁止指令的重排序,从而使得多线程中不会出现乱序的问题;

我们可以看到,加锁和volatile最大的区别就是原子性;

主要是因为volatile只是针对某个变量进行修饰,所以就有点像原子变量的复合操作(虽然原子变量本身是原子操作,但是多个原子变量放到一起,就无法保证了)

总结

  1. 可见性在单线程中没问题,但是多线程会有问题
  2. volatile是一种比加锁轻量级的同步机制,可以保证变量的可见性和有序性(禁止重排序)
  3. 指令重排序:有时虚拟机为了优化性能,会在运行时把相互没有依赖的代码顺序重新排序,以此来减少指令的等待时间,提高效率
  4. 加锁和volatile的区别:加锁可以保证原子性,volatile不可以

参考内容:

  • 《Java并发编程实战》
  • 《实战Java高并发》

后记

最后,感谢大家的观看,谢谢

原创不易,期待官人们的三连哟

对象的可见性 - volatile篇的更多相关文章

  1. jvm(三)指令重排 & 内存屏障 & 可见性 & volatile & happen before

    参考文档: https://tech.meituan.com/java-memory-reordering.html http://0xffffff.org/2017/02/21/40-atomic- ...

  2. JAVA并发编程学习笔记------对象的可见性及发布逸出

    一.非原子的64位操作: 当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值,这种安全性保证被称为最低安全性.最低安全性适用于绝大多数变量 ...

  3. 多线程(三)~多线程中数据的可见性-volatile关键字

    我们先来看一段代码: ①.线程类,用全局布尔值控制线程是否结束,每隔1s打印一次当前线程的信息 package com.multiThread.thread; publicclassPrintStri ...

  4. Java内存可见性volatile

    概述 JMM规范指出,每一个线程都有自己的工作内存(working memory),当变量的值发生变化时,先更新自己的工作内存,然后再拷贝到主存(main memory),这样其他线程就能读取到更新后 ...

  5. 深入理解javascript对象系列第三篇——神秘的属性描述符

    × 目录 [1]类型 [2]方法 [3]详述[4]状态 前面的话 对于操作系统中的文件,我们可以驾轻就熟将其设置为只读.隐藏.系统文件或普通文件.于对象来说,属性描述符提供类似的功能,用来描述对象的值 ...

  6. Spring.NET学习笔记7——依赖对象的注入(基础篇) Level 200

    1.person类 public class Person    {        public string Name { get; set; }        public int Age { g ...

  7. 单例模式-全局可用的 context 对象,这一篇就够了

    单例模式在各个方面都有着极为广泛的使用,所谓单例,顾名思义就是整个程序中只有一个该类的实例,所以它成功保证了整个程序的生命周期内该类的对象只能创建一次,并且提供全局唯一访问该类的方法:getInsta ...

  8. 可见性-volatile

    出处: http://blog.csdn.net/vking_wang/article/details/9982709

  9. Java并发:volatile内存可见性和指令重排

    volatile两大作用 1.保证内存可见性 2.防止指令重排 此外需注意volatile并不保证操作的原子性. (一)内存可见性 1 概念 JVM内存模型:主内存和线程独立的工作内存 Java内存模 ...

随机推荐

  1. Java 搭建 RabbitMq 消息中间件

    前言 当系统中出现"生产"和"消费"的速度或稳定性等因素不一致的时候,就需要消息队列. 名词 exchange: 交换机 routingkey: 路由key q ...

  2. Spring-03 依赖注入(DI)

    Spring-03 依赖注入(DI) 依赖注入(DI) 依赖注入(Dependency Injection,DI). 依赖 : 指Bean对象的创建依赖于容器,Bean对象的依赖资源. 注入 : 指B ...

  3. 使用gitlab构建基于docker的持续集成(三)

    使用gitlab构建基于docker的持续集成(三) gitlab docker aspnetcore 持续集成 构建发布思路: aspnetcore 下的dockerfile编写 发布docker- ...

  4. Python 过滤字母和数字

    [前言]在写爬虫时,正则表达式有时候比较难写,一个是自己不熟练,二者数据分析提取数据千奇百怪. 一.好在python有个re模块,提供了很多更加简便的方法:可参考此文档:https://www.cnb ...

  5. localforage indexedDB如何使用索引

    简单介绍下localForage.localForage 是一个 JavaScript 库,通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验.它能存储多 ...

  6. CentOS Install NMP

    目录 Installation steps of the Nginx install run 默认安装路径 指定安装目录 Installation steps of the MySQL 下载源码包 解 ...

  7. PAT-1066(Root of AVL Tree)Java语言实现

    Root of AVL Tree PAT-1066 这是关于AVL即二叉平衡查找树的基本操作,包括旋转和插入 这里的数据结构主要在原来的基础上加上节点的高度信息. import java.util.* ...

  8. MVC base64加密的文件,前端下载

    后端代码: public FileResult OutPutFile(string  base64file,string filename) { buffer = Convert.FromBase64 ...

  9. 使用python模块plotdigitizer抠取论文图片中的数据

    技术背景 对于各行各业的研究人员来说,经常会面临这样的一个问题:有一篇不错的文章里面有很好的数据,但是这个数据在文章中仅以图片的形式出现.而假如我们希望可以从该图片中提取出数据,这样就可以用我们自己的 ...

  10. C指针与二维数组

    先贴上完整的代码: #include<stdio.h> int main(int argc, char *argv[]){ int a[3] [5]={1,2,3,4,5,6,7,8,9, ...