对volatile的理解

栏目: Java · 发布时间: 6年前

内容简介:JMM(java内存模型 Java Memory Model)本身是一种抽象的概念,描述一组规则后规范通过这组规范定义了程序中各个变量(包括实例字段,静态变量和组成数组对象的元素)的访问方式。JMM关于同步的规定:由于JMM运行程序的实体是线程,而每个线程创建JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在

JMM(java内存模型 Java Memory Model)本身是一种抽象的概念,描述一组规则后规范通过这组规范定义了程序中各个变量(包括实例字段,静态变量和组成数组对象的元素)的访问方式。

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加解锁是同一把锁

由于JMM运行程序的实体是线程,而每个线程创建JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量都存储在 主内存 ,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取或赋值)必须在工作内存中进行,首先要将内存从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(值传递)必须通过主内存来完成。访问过程如下:

对volatile的理解

JMM三个特性(约束)

  1. 可见性
  2. 原子性
  3. 有序性

volatile是什么

volatile是Java虚拟机提供的轻量级同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

什么是可见性

public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {
        MyData myData = new MyData();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.add60();
            System.out.println(Thread.currentThread().getName()+ "  线程内修改num的值为 :" + myData.number);
        }, "Thread1").start();

        while (myData.number == 0) {

        }
        TimeUnit.SECONDS.sleep(4);
        System.out.println(Thread.currentThread().getName()+ "   主线程从循环中跳出");
    }
}

class MyData {
    volatile int number = 0;

    void add60() {
        this.number = 60;
    }
}
复制代码

以上代码执行结果为

Thread1  线程内修改num的值为 :60
main   主线程从循环中跳出
复制代码

由结果可知,线程之间是有可见性的,即线程Thread1 修改了number的值为60,主线程的工作内存中的number读到修改后的值为60,便跳出循环,输出循环外的语句。

如果将classData中的 number 去掉volatile的修饰,则线程间没有可见性,即主线程读不到number修改后的值,main的工作内存中还是保留number值为0,则一直停留在循环中。

什么是原子性

不可见分割,完整性,某个线程正在做某个具体业务时,中间不可被加塞或者分割,需要整体完成,要么成功,要么失败。

public class VolatileAtomicity {
    public static void main(String[] args) {
        MyDatas myDatas = new MyDatas();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myDatas.numPlusPlus();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(myDatas.number);
    }
}

class MyDatas {
    volatile int number = 0;

    void numPlusPlus() {
        number++;
    }
}
复制代码

以上代码执行结果总是小于2000

首先我们知道 number++ 操作是分两步执行:

  1. 获取当前number的值
  2. number++
  3. 结果放回主内存中。
    根据上述,volatile修饰的number是不保证原子性的,也就是在执行number++的过程会被其他线程打断,即A线程获取number的值为1时,同时B线程也获取了number的值为1,A进行第二步number+1,然后第三步准备将结果2放回主内存时,线程B抢先一步将其工作内存中number+1的结果2放回主内存,理论上A/B两个线程各加了1,结果应该为3,但是实际上A线程计算结果也是2,放回主内存时只是覆盖了B线程的结果,最终主内存中的结果在经过两个线程分别+1后还是2.

以上,可知volatile是不符合JMM规定的原子性的,同时number++在多线程下是非线程安全的。

如何解决原子性问题

  • 加synchronized
  • 使用原子类AtomicInteger
    使用synchronized对于此场景来说太重,因此优先考虑使用AtomicInteger作为number的类型。

有序性

volatile的有序性表现在 禁止指令重排    
复制代码

指令重排

计算机在执行程序时,为了提高性能,编译器和处理器常常会对 指令做重排 ,一般分为三类:

对volatile的理解

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致

处理器在进行重 排序 时必须考虑指令间的 数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

void mysort() {
        int x = 11;
        int y = 12;
        x = x + 5;
        y = x * x;
    }
复制代码

以上代码,执行顺序 1234、2134、1324 是不影响最终的结果的,因此代码编译后指令顺序不一定为1234,这种行为在多线程下可能会造成结果的误差。

public class ReSoreSeqDemo {
    int a = 0;
    boolean flag = false;

    public void method1() {
        a = 1;
        flag = true;
    }

    public void method2() {
        if (flag) {
            a = a + 5;
            System.out.println("****value = " + a);
        }
    }
}
复制代码

如上代码,现有两个线程A/B,分别执行method1和2,仅对于线程A来说,method1方法两行执行顺序先后没有关系。但是如果method1顺序变了,先执行 flag=true ,这时线程B就进入method2的判断中,a=5,再接着执行线程A的 a=1 ,最后结果为 a = 1,与期望结果 a=1 然后 a = a+5 的结果有差异。这就是需要禁止指令重排的原因。

volatile实现 禁止指令重排优化 ,从而避免多线程环境下环境出现乱序执行的现象。

内存屏障(Memory Barrier)

内存屏障又称内存栅栏,是一个CPU指令,他的作用有两个:

  1. 保证特定操作的执行顺序
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条MemoryBarrier则会告诉编译器和CPU, 不管什么指令都不能和这条MemoryBarrier指令重排序,也就是 通过内存屏障禁止在内存屏障前后的指令执行重排序优化 。内存屏障另外的一个作用是强制刷出各种CPU的焕醋拿数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量之刷新到主内存中;

对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

线程安全如何获得保障

工作内存和主内存同步延迟现象导致的可见性问题

  • 可以使用synchronized或volatile关键字解决,他们都可以是一个线程修改后的变量立即对其他线程可见

对于指令重排导致的可见性问题和有序性问题

  • 可以利用volatile关键字解决,因为volatile的另一个作用就是禁止重排指令优化

本文结束

内容根据尚硅谷视频总结

欢迎访问我的个人博客justd的博客


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Host Your Web Site In The Cloud

Host Your Web Site In The Cloud

Jeff Barr / SitePoint / 2010-9-28 / USD 39.95

Host Your Web Site On The Cloud is the OFFICIAL step-by-step guide to this revolutionary approach to hosting and managing your websites and applications, authored by Amazon's very own Jeffrey Barr. "H......一起来看看 《Host Your Web Site In The Cloud》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具