• Java多线程 深入理解volatile关键字 volatile的使用和原理分析


    volatile最重要的一个作用就是保证可见性。但不保证原子性。

    先简单说一下什么是可见性,可见性是指有多个线程共享同一个变量,那么其中任意一个线程修改了共享变量,其它的线程都能看到最新的数据,而不是自己的备份缓存。这就是可见性。 默认情况下,多线程共享变量是互相不可见的,也就是说,一个线程对于共享变量的修改,别的线程并不能获取到最新的值,这就会造成并发问题。volatile就是专门用来解决这种可见性问题的。

    synchronized也能保证可见性,而且还能保证原子性。也可以理解成volatile是一种简单版的synchronized,只单单保证可见性,不保证原子性。可以说定位清晰,因为功能更简单,所以性能也更好。

    这里还要简单说一下原子性。原子性是指在一次操作或多次操作中,要么所有的操作全部都得到了执行并不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可分割的整体。 比如num++操作,num=num+1,实际上是有三个步骤,先读取num,在计算+1,在写回内存中,这种操作就不是原子的,因为可以分解为更小的操作。要让它变成原子操作也是可以的就是加锁,比如synchronized,加锁后num++就可以不被干扰,一口气执行完了。

    下面举一个经典例子看怎么用volatile保证可见性。
    下面这个例子有两个线程,第一个线程对num进行5次++操作。第二个线程有一个死循环,直到flag变为true才会退出循环并输出num最后的值。这是理想情况,如果运行代码将会看到下面的结果。

    public class TestVolatileVisual {
        private int num;
        private boolean flag=false;
    
        public void addNum(){
            num++;
        }
    
        public int getNum(){
            return num;
        }
    
        public static void main(String[] args) {
            TestVolatileVisual tvv=new TestVolatileVisual();
            new Thread(()->{
                for (int i = 0; i < 5; i++) {
                    tvv.addNum();
                    try {
                        //这里必须睡眠,不然因为线程执行太快,flag并没有成为共享变量
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("num = "+tvv.getNum());
    
                }
                tvv.flag=true;
                System.out.println("flag 已经设置为true");
            }).start();
            new Thread(()->{
                while (!tvv.flag){
                    //
                }
                System.out.println("now num===="+tvv.getNum());
            }).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
    • 34
    • 35
    • 36
    • 37

    第二个线程的一直没有输出,而且程序没有退出,实际上,线程停在第二个线程的死循环里面。对于线程二来说,flag的值一直是false。并不直到已经被线程一修改为true了。这就是典型的可见性问题。共享变量flag被线程一修改,线程二不知道,还是用自己缓存的flag值。从而导致程序出现错误。

    num = 1
    num = 2
    num = 3
    num = 4
    num = 5
    flag 已经设置为true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个问题解决起来非常简单,都是缓存惹的祸,只要保证缓存每次修改的时候强制刷新进主存就行,volatile就是为了实现这样的功能而设计的。
    只要对flag添加volatile关键字就可以解决这个问题。volatile能够保证每次对于被他修饰的变量的修改都能强制刷新进主存,从而解决了可见性问题。

        private volatile boolean flag=false;
    
    
    • 1
    • 2

    添加volatile关键字后,程序就按理想的样子运行了。

    num = 1
    num = 2
    num = 3
    num = 4
    num = 5
    flag 已经设置为true
    now num====5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    synchronized也能保证可见性,也可以实现上面的功能,但用volatile更合适,用synchronized是大材小用。
    下面给出实例代码,用synchronized修饰flag的get set方法就可以了。别添加错了,最主要的共享变量是flag,而不是num。

    public class TestVolatileVisualBySync {
        private int num;
        private  boolean flag=false;
    
        public synchronized boolean getFlag() {
            return flag;
        }
    
        public synchronized void setFlag(boolean flag) {
            this.flag = flag;
        }
    
        public void addNum(){
            num++;
        }
    
        public int getNum(){
            return num;
        }
    
        public static void main(String[] args) {
            TestVolatileVisualBySync tvv=new TestVolatileVisualBySync();
            new Thread(()->{
                for (int i = 0; i < 5; i++) {
                    tvv.addNum();
                    try {
                        //这里必须睡眠,不然因为线程执行太快,flag并没有成为共享变量
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("num = "+tvv.getNum());
    
                }
                tvv.setFlag(true);
                System.out.println("flag 已经设置为true");
            }).start();
            new Thread(()->{
                while (!tvv.getFlag()){
                    //
                }
                System.out.println("now num===="+tvv.getNum());
            }).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
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    下面来说说volatile不能保证原子性,上面的例子num变量并没有出现并发问题是因为,两个线程并没有同时操作num,第二个线程要操作num的时候,第一个线程已经结束了。实际上num++是操作并发问题的,因为不能保证原子性。其实验证这个比较无聊,下面给出简单的验证例子。
    这个例子第一次见的话还是有点不好理解的,有10个线程都各种执行100次,因为是并行执行,所以10个线程都执行完,也只花费了100ms多。而main线程的10次for循环,每次花费就已经100ms了,也就是说,main线程的头两次循环差不多就看到效果了,后面的只是凑个数。观察到的现象就是num最终的结果并不是1000,这就是声明出现了并发问题。如果给addNum方法加上synchronized,结果就会是1000,就不存在并发问题,也就是说volatile不能保证原子性,而synchronized能保证原子性。

    public class TestVolatileAtomic {
        private int num;
        private volatile boolean flag=false;
    
        public void addNum(){
            num++;
        }
    
        public int getNum(){
            return num;
        }
        public static void main(String[] args) {
            TestVolatileAtomic ta=new TestVolatileAtomic();
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    for (int j = 0; j < 100; j++) {
                        try {
                            Thread.sleep(1);
                            ta.addNum();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
    
                }).start();
            }
    
            for (int i = 0; i < 11; i++) {
                try {
                    Thread.sleep(100);
                    System.out.println(ta.getNum());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            
        }
    }
    
    • 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
    • 35
    • 36
    • 37
    • 38

    输出是不确定的,只需要观察头几次就可以。

    890
    988
    988
    988
    988
    988
    988
    988
    988
    988
    988
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上面的例子已经很好的演示了volatile的用法和它的作用。下面就需要对volatile背后的原理做一些解释。

    volatile原理

    我们先从volatile的特性开始讲起。
    总的来说,volatile是一种轻量级的同步机制。具有下面的特点:
    1.保证可见性。
    被volatile修饰的变量会强制刷新进主存,不会被缓存,这样就保证了可见性。
    2.不保证原子性。
    使用volatile修饰仍然是线程不安全的。
    3.禁止指令重排。
    不用担心因为重排而带来的有序性问题。

    保证可见性

    可见性指的是一个线程对于一个共享变量的修改,另一个线程能立即看到。
    1.对于一个被volatile修饰的变量的读操作,读取到的数据总是最新的,也就是被修改过的数据。
    2.一个线程去读取一个被volatile修饰的变量的时候,该变量在工作内存的数据无效,会从主内存读取最新的数据。
    3.对于一个被volatile修饰的变量的写操作,写入的数据会立即刷新到主内存中。

  • 相关阅读:
    《canvas》之第18章 高级动画
    摩尔线程推出首款数据中心级全栈功能GPU:MTT S2000
    固态硬盘SSD
    内网学习笔记(4)
    Hive的安装与配置——第1关:Hive的安装与配置
    面试必问:MySQL死锁 是什么,如何解决?(史上最全)
    正则表达式对字符串处理
    微服务架构-微服务架构的挑战与微服务化的具体时机
    (k8s中)docker netty OOM问题记录
    CEC2013(MATLAB):猎豹优化算法(The Cheetah Optimizer,CO)求解CEC2013
  • 原文地址:https://blog.csdn.net/ScottePerk/article/details/125556499