• volatile可见性和指令重排


    volatile关键字的2个作用

    1.线程的可见性 

    2.防止指令重排

    什么是线程的可见性?

    线程的可见性 就是一个线程对一个变量进行更改操作 其他线程获取会获得最新的值。

    线程在执行的行 操作主线程的变量。会将变量的副本拷贝一份到线程的工作区域(避免每次到主线程读取 提高效率),在更改后的一段时间内写入主内存

    如下示例代码:

    public class Accounting implements Runnable {
         boolean quit=false;
         int i=0;
        @Override
        public void run() {
            while (!quit){
                i++;
            }
            System.out.println("线程退出");
        }
        public static void main(String[] args) throws InterruptedException {
            Accounting accounting = new Accounting();
            Thread a1 = new Thread(accounting, "a1");
            Thread a2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000);
                        System.out.println("开始通知线程结束");
                        accounting.setQuit(true);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            a2.start();
            a1.start();
            Thread.sleep(1000);
        }
    
        public boolean isQuit() {
            return quit;
        }
    
        public void setQuit(boolean quit) {
            this.quit = quit;
        }
    }

    这段代码的逻辑就是线程a1 执行循环操作  a2 2秒后设置quit为true任务结束 打印 "线程退出";

    那么真的能够成功退出吗?我们看看 线程执行在内存中的操作图

    打印:

    开始通知线程结束

    a2 线程首先将自己工作线程的quit改为ture ,然后一定时间之后去将主内存的quit改为true  ,但是a1线程始终是操作的是自己的工作内存的副本 所以死循环

    这个时候在quit加上volatile关键字

      volatile boolean quit=false;

    打印

    开始通知线程结束
    线程退出

    加上volatile关键字后。当一个线程对变量进行修改会更新自己的工作内存里面的值,然后立即将改动的值刷新到主内存,同时线程2的工作内存的quit副本缓存失效  下次直接到主内存读取  所以能够正常执行

    记录一个小插曲

    System.out.println,sychronized,Thread.sleep Thread.sleep 影响可见性?

    System.out.println

    public class Accounting implements Runnable {
         boolean quit=false;
         int i=0;
        @Override
        public void run() {
    while (!quit){ i++; System.out.println(i); } System.out.println("线程退出"); } public static void main(String[] args) throws InterruptedException { Accounting accounting = new Accounting(); Thread a1 = new Thread(accounting, "a1"); Thread a2 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); System.out.println("开始通知线程结束"); accounting.setQuit(true); } catch (InterruptedException e) { e.printStackTrace(); } } }); a2.start(); a1.start(); Thread.sleep(1000); } public boolean isQuit() { return quit; } public void setQuit(boolean quit) { this.quit = quit; }

    会发现没有加上volatile一样可以成功退出 。那我们上面说的 线程的内存处理 不成立了吗?

    查资料说 是因为jvm对锁的优化。因为如果我们在循环里面加上sychronize同步锁 会产生大量的锁竞争 所以jvm优化过后

       synchronized (this){
                while (!quit){
                  //.....
                }
            }

    但是我们并没有在while里面加锁啊。我们看看打印的方法源码

        public void println(int x) {
            synchronized (this) {
                print(x);
                newLine();
            }
        }

    sleep方法并没有加锁,为什么能够保证可见性

    sleep是阻塞线程并不释放锁,让出cpu调度。 让出cpu调度后下次执行会刷新工作内存

    指令重排

    指令重排指在编译的时候,在不单线程运行不影响结果的情况下进行指令优化

    如:

    public class Context {
        boolean isLoad=false;
        Object configuration=null;
        public void   loadConfiguration(){
            System.out.println("正在加载配置文件");
            configuration=  new Object();
            isLoad=true;
        }
    
        public void  initContext(){
            System.out.println("正在进行初始化");
        }
    
        public static void main(String[] args) {
           Context context=new Context();
           context.loadConfiguration();
           if(context.isLoad){
               context.initContext();
           }
        }
    }

    这段代码就是先加载配置文件信息  然后初始化上下文

    我们在单线程下 把他们的顺序调换模拟指令重排 会对结果没有影响

      public void   loadConfiguration(){
            isLoad=true;
            System.out.println("正在加载配置文件");
            configuration=  new Object();
           
        }

    但是在多线程下面

    public class Context {
         boolean isLoad=false;
        Object configuration=null;
        public void   loadConfiguration(){
            //模拟jvm指令重排 将isLoad命令排在第一位
            isLoad=true;
            /***
             * 模拟并发情况下指令重排。导致的isload=true排到前面。
             * 这个时候配置文件没初始化。initContext监听到lsLoad等于true根据配置文件进行初始化
             */
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            configuration=  new Object();
            //isLoad=true;指令重排前
        }
    
        public void  initContext(){
            configuration.toString();
            System.out.println("正在进行初始化");
        }
    
        public static void main(String[] args) {
            Context context=new Context();
            //负责监听 如果加载完毕 则进行上下午初始化
            Thread t2=new Thread(new Runnable() {
                @Override
                public void run() {
    
                        while (true){
                            if(context.isLoad){
                                context.initContext();
                                break;
                            }
                        }
    
    
                }
            },"t2");
            //负责加载配置文件
            Thread t1=new Thread(new Runnable() {
                @Override
                public void run() {
                    context.loadConfiguration();
                }
            },"t1");
            t1.start();
            t2.start();
        }
    }

    只是模拟指令重排 先不考虑可见性  这种情况会初始化context 没有configuration 报错  使用volatile关键字修饰可以避免

    值得注意的一点

    volatile虽然能够保证线程的可见性 但是并不能保证原子性  比如i++操作 都是读出i的值 进行运算再写入。如果在读出的时候别的线程改变了 就会不一致

    哪种场景适合用volatile 对一个变量的值进行修改 不依赖其他值。 比如 index=true   而不是i=i+j;或则index=j>a   或 a=j (会从内存中读出j的值 然后赋值到a);

    java提供atomic cas能够性能比锁高能够保证原子性 如:atomicInt atomictDouble

  • 相关阅读:
    http headers总结
    golang跨域配置
    Kafka中topic的Partition,Kafka为什么这么快,Consumer的负载均衡及consumerGroup的概念
    kafka partition(分区)与 group
    RocketMQ从部署到应用(Golang)
    Codeforces Round #706 (Div. 2)
    关于平衡树
    具体数学 第三章 整值函数
    FFT&NTT&多项式全家桶
    省选测试15
  • 原文地址:https://www.cnblogs.com/LQBlog/p/8718735.html
Copyright © 2020-2023  润新知