zoukankan      html  css  js  c++  java
  • Java内存可见性volatile

    概述

    JMM规范指出,每一个线程都有自己的工作内存(working memory),当变量的值发生变化时,先更新自己的工作内存,然后再拷贝到主存(main memory),这样其他线程就能读取到更新后的值了。
    注意:工作内存和主存是JMM规范里抽象的概念,在JVM的内存模型下,可以将CPU缓存对应作线程工作内存,将JVM堆内存对应主存。

    写线程更新后的值何时拷贝到主存?读线程何时从主存中获取变量的最新值?hotspotJVM中引入volatile关键字来解决这些问题,当某个变量被volatile关键字修饰后,多线程对该变量的操作都将直接在主存中进行。在CPU时钟顺序上,某个写操作执行完成后,后续的读操作一定读取的都是最新的值。

    内存可见性带来的问题

    如下代码片段,写线程每隔1秒递增共享变量counter,读线程是个死循环,如果读线程始终能读取到counter的最新值,那么最终的输出应该是 12345。

    public class App {
        // 共享变量
        static int counter = 0;
    
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                int temp = 0;
                while (true) {
                    if (temp != counter) {
                        temp = counter;
                        // 打印counter的值,期望打印 12345
                        System.out.print(counter);
                    }
                }
            });
    
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    counter++;
                    // 等待1秒,给读线程足够的时间读取变量counter的最新值
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
    
                // 退出程序
                System.exit(0);
            });
    
            thread1.start();
            thread2.start();
        }
    }
    

    在没有volatile的情况下,实际的输出结构如下:

    1
    
    Process finished with exit code 0
    

    通过volatile解决问题

    将共享变量用volatile关键字修饰即可,如下:

    // 共享变量
    static volatile int counter = 0;
    

    再次执行程序,输出结果如下:

    12345
    
    Process finished with exit code 0
    

    综上,volatile关键字使得各个线程对共享变量的操作变得一致。在非volatile字段上做更新操作时,无法保证其修改后的值何时从工作内存(CPU缓存)刷新到主存。对于非volatile字段的读操作也是如此,无法保证线程何时从主存中读取最新的值。

    volatile无法保证线程安全性

    如下代码片段,多个线程同时递增一个计数器:

    public class App {
        // 共享变量
        static volatile int counter = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter++;
                }
            });
    
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter++;
                }
            });
    
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
    
            System.out.println("总和:" + counter);
        }
    

    输入结果:

    总和:12374
    

    如果volatile能保证线程安全,那么输出结果应该是20000,但上面的代码输出12374,所以说,volatile不能解决线程安全(thread)的问题。
    所以,还是要通过其他手段来解决多线程安全的问题,比如synchronized。

    volatile和synchronized的区别

    在上述的代码示例中,我们并没有涉及到多线程竞态(race condition)的问题,核心点是“多线程情况下,对共享变量的写入如何被其他线程及时读取到”。
    synchronized关键字是Java中最常用的锁机制,保证临界区(critical section)中的代码在同一个时间只能有一个线程执行,临界区中使用的变量都将直接从主存中读取,对变量的更新也会直接刷新到主存中。所以利用synchronized也能解决内存可见性问题。
    代码如下:

    public class App {
        // 共享变量
        static int counter = 0;
    
        public static void main(String[] args) {
            // 读取变量的线程
            Thread readThread = new Thread(() -> {
                int temp = 0;
                while (true) {
                    synchronized (App.class) {
                        if (temp != counter) {
                            temp = counter;
                            // 打印counter的值,期望打印 12345
                            System.out.print(counter);
                        }
                    }
                }
            });
    
            // 修改变量的线程
            Thread writeThread = new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    synchronized (App.class) {
                        counter++;
                    }
    
                    // 等待1秒,给读线程足够的时间读取变量counter的最新值
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
    
                System.exit(0);
            });
    
            readThread.start();
            writeThread.start();
        }
    }
    

    运行,输入结果:

    12345
    
    Process finished with exit code 0
    

    虽然通过synchronized也能解决内存可见性的问题,但是这个解决方案也带来了其他问题,比如性能会比较差。

    总结

    多线程可以提升程序的运行速度,充分利用多核CPU的算力,但多线程也是“恶魔”,会给程序员带来很多问题,比如本文中的内存可见性问题。volatile可以使变量的更新及时刷新到主存,变量的读取也是直接从主存中获取,保证了数据的内存一致性。但是volatile不是用来解决线程安全问题的,无法替代锁机制。

    参考:
    [1] Java Memory Model - Visibility problem, fixing with volatile variable
    [2] Guide to the Volatile Keyword in Java
    [3] Managing volatility
    [4] Java Volatile Keyword
    [5] Thread and Locks

  • 相关阅读:
    用存储过程生成记录编号
    【转载】JavaScript实现密码强度检测
    【转载】汇编写的3D动画
    【整理】VS2005调试出现“无法附加 绑定句柄无效”错误解决办法
    【原创】C#中ref和out的异同
    【整理】UpdatePanel中验证控件失效问题
    【摘录】asp.net Cookie操作
    【摘录】asp.net Cookie操作(续)
    【原创】C#操作注册表(演示操作启动项)
    【整理】asp.net web.config加解密
  • 原文地址:https://www.cnblogs.com/junejs/p/12686902.html
Copyright © 2011-2022 走看看