当前位置:   article > 正文

多线程进阶:volatile的作用以及实现原理_多线程共享变量volatile

多线程共享变量volatile


提示:以下是本篇文章正文内容,Java系列学习将会持续更新

volatile的作用

 1. volatile能保证变量在多线程之间的可见性。
 2. volatile禁止CPU执行时进行指令重排操作(内存屏障)从而能保证有序执性。
 3. volatile不能保证原子性

一、volatile实现内存可见性

内存可见性问题

内存可见性问题:在多线程共享一个数据块下,一个线程对数据进行修改操作时,其它线程是无法感知的。甚至会被编译器优化到完全不可见的程度。
 例如:当两个线程同时操作一个内存,例如一个读一个写,但是当“写线程”进行的修改的时候,“读线程”可能读到修改前的数据,也可能读到修改后的数据,这是不确定的。
不可见的原因
 CPU为了提高数据获取速率,会设置缓存。
 在多核CPU下,每个核都有自己的独占缓存进行数据存取,只有在所有处理结束后,才会将数据同步到主存中。
 所以会导致有些核读取到的是过期的数据。

volatile的原理

简单来说:当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。
当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。
当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取

内部如何做到缓存主存同步的?
缓存一致性协议(MESI):当CPU写数据时,如果发现操作的变量时共享变量,即其他线程的工作内存也存在该变量,于是会发信号通知其他CPU该变量的内存地址无效。当其他线程需要使用这个变量时,如内存地址失效,那么它们会在主存中重新读取该值。

现象

import java.util.concurrent.TimeUnit;
public class Demo {
    // 保证内存可见性
    public static volatile boolean flag = true;

    static class Thread1 extends Thread {
        @Override
        public void run() {
            while (flag) {
                // 一直循环,等待flag被改变
            }
            System.out.println("线程1发现flag被改变了。。。");
        }
    }
    static class Thread2 extends Thread {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = false;
            System.out.println("线程2修改了flag的值。。。");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread1 t1 = new Thread1();
        Thread2 t2 = new Thread2();
        t2.start();
        t1.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

回到目录…

二、volatile禁止代码重排序

指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。
 这在单线程中不会构成问题,但是在多线程中就会出现问题。
 非常经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。
private volatile static LazyModeV3 instance;

对象实例化的步骤

memory = allocate(); // 1.分配对象内存空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null
  • 1
  • 2
  • 3

代码重排序后的步骤

memory=allocate(); // 1.分配对象内存空间
instance=memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); // 2.初始化对象
  • 1
  • 2
  • 3

volatile的原理

volatile禁止重排序是利用内存屏障,保证有序性。
内存屏障是一组CPU指令,用于实现对内存操作的顺序限制。
Java编译器在生成指令系列时,在适当的位置会插入内存屏障来禁止处理器对指令的重新排序。

(1)volatile会在变量写操作的前后加入两个内存屏障,来保证前面的写指令和后面的读指令是有序的。
在这里插入图片描述

(2)volatile在变量的读操作后面插入两个指令,禁止后面的读指令和写指令重排序。
在这里插入图片描述

回到目录…

三、volatile不支持原子性

直接上代码,看现象

public class Test {
    public static volatile int i = 0;

    static class Thread1 extends Thread {
        @Override
        public void run() {
            for (int j = 0; j < 1000000; j++) {
                i ++;
            }
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            for (int j = 0; j < 1000000; j++) {
                i --;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread1 t1 = new Thread1();
        t1.start();
        Thread2 t2 = new Thread2();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
        // 结果是随机数,说明volatile不支持原子性
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

总结:volatile不能保证原子性,要想保证原子性我们要使用锁机制

回到目录…


总结:
提示:这里对文章进行总结:
以上就是今天的学习内容,本文是Java多线程的学习,深入学习了关键字volatile的作用以及原理。之后的学习内容将持续更新!!!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/266083
推荐阅读
相关标签
  

闽ICP备14008679号