关于java:对象的可见性-volatile篇

49次阅读

共计 3525 个字符,预计需要花费 9 分钟才能阅读完成。

作者:汤圆

集体博客: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 高并发》

后记

最初,感激大家的观看,谢谢

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

正文完
 0