对象的可见性 - volatile篇

做者:汤圆java

我的博客: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)以后,再读取就变成了10this

0
10

改了就能看到,若是多线程也有这么简单,那多好(来自菜鸟的心里独白)。spa

下面咱们看一个多线程的例子,仍是那个共享变量

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看作一个标志,若是虚拟机看到这个标志,就会认为被它修饰的变量是易变的,不稳定的,随时可能被某个线程修改;

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

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

可见性问题-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高并发》

后记

最后,感谢你们的观看,谢谢

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

相关文章
相关标签/搜索