专栏首页汤圆学Java对象的可见性 - volatile篇
原创

对象的可见性 - volatile篇

作者:汤圆

个人博客: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高并发》

后记

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

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

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 对象可见性

    此引出 Java 的一个一般设计原则——对象默认可见。如果我有一个对象的引用,就可以复制一个副本,然后将其交给另一个线程,不受任何限制。Java 中的引用其实就...

    宇宙之一粟
  • Volatile 可见性承诺

    Java Volatile 关键字是一种轻量级的数据一致性保障机制,之所以说是轻量级的是因为 volatile 不具备原子性,它对数据一致性的保障体现在对修改过...

    不会飞的小鸟
  • Java多线程--对象的可见性

      最近在看《Java并发编程实战》,并发方面的知识,今天看到了对象的可见性,在这里分享一下。

    haoming1100
  • 【不要再背】volatile的可见性和原子性

    volatile保证可见性的原理是在每次访问变量时候都会进行一次刷新,因此每次访问都是准没存中最新的版本,所以volatile关键字的作用之一就是保证变量修改的...

    田维常
  • Java多线程之可见性之volatile

    入门小站
  • 请谈谈你对线程可见性及volatile关键字的理解?

    引言可见性问题基本数据类型的可见性问题引用数据类型可见性问题引用可见性问题成员变量可见性问题可见性问题总结Java内存模型CPU与内存之间的爱恨情仇Java内存...

    敲得码黛
  • 多线程之内存可见性Volatile(一)

    从这篇博文开始,我们开始分享一些多线程的内容,毕竟在工作中,使用多线程比较多。多总结一下,终归没有坏处。这个系列的文章不会特别长,争取在3到5分钟之间结束,主要...

    程序猿小亮
  • 面经手册 · 第14篇《volatile 怎么实现的内存可见?没有 volatile 一定不可见吗?》

    从前的能吃苦大多指的体力劳动的苦,但现在的能吃苦已经包括太多维度,包括:读书学习&寂寞的苦、深度思考&脑力的苦、自律习惯&修行的苦、自控能力&放弃的苦、低头做人...

    小傅哥
  • 内存可见性和原子性:Synchronized和Volatile的比较

    【尊重原创,转载请注明出处】http://blog.csdn.net/guyuealian/article/details/52525724

    traffic
  • Java并发编程之验证volatile的可见性

    通过系列文章的学习,凯哥已经介绍了volatile的三大特性。1:保证可见性 2:不保证原子性 3:保证顺序。那么怎么来验证可见性呢?本文凯哥将通过代码演示来证...

    凯哥Java
  • 深入理解Java多线程中的volatile关键字Java 的 volatile关键字对可见性的保证Java 的 volatile关键字在保证可见性之前的所做的事情Volatile有时候也是不够的什么时

    Java关键字用于将一个变量标记为“存储在内存中的变量”。更准确的说,意思就是每一次对volatile标记的变量进行读取的时候,都是直接从电脑的主内存进行的,而...

    desperate633
  • 猫头鹰的深夜翻译:Volatile的原子性, 可见性和有序性

    为什么要额外写一篇文章来研究volatile呢?是因为这可能是并发中最令人困惑以及最被误解的结构。我看过不少解释volatile的博客,但是大多数要么不完整,要...

    眯眯眼的猫头鹰
  • Java并发之原子变量及CAS算法-上篇

    本文主要讲在Java并发编程的时候,如果保证变量的原子性,在JDK提供的类中式怎么保证变量原子性的呢?。对应Java中的包是:java.util.concurr...

    凯哥Java
  • 并发编程-06线程安全性之可见性 (synchronized + volatile)

    并发编程-06线程安全性之可见性 (synchronized + volatile)

    小小工匠
  • 并发编程进阶二:搞定可见性、有序性问题,用'它'就够了!!!

    承接上一篇"并发的三个潜在问题:CPU缓存引发的可见性、线程切换引发的原子性、编译优化引发的有序性"。

    浩说编程
  • Go 语言面向对象教程 —— 类属性和方法的可见性

    前面我们已经陆续介绍了 Go 语言中面向对象的基本特性,包括自定义类的实现、构造函数、成员方法、类的继承、方法重写等,今天我们来系统介绍下类的属性和成员方法的可...

    学院君
  • 并发编程的艺术

    关于 Java 并发也算是写了好几篇文章了,本文将介绍一些比较基础的内容,注意,阅读本文需要一定的并发基础。

    技术zhai
  • 并发编程的艺术

    关于 Java 并发也算是写了好几篇文章了,本文将介绍一些比较基础的内容,注意,阅读本文需要一定的并发基础。

    技术zhai
  • 既生synchronized,何生volatile?!

    在我的博客和公众号中,发表过很多篇关于并发编程的文章,之前的文章中我们介绍过了两个在Java并发编程中比较重要的两个关键字:synchronized和volat...

    Java技术江湖

扫码关注云+社区

领取腾讯云代金券