zoukankan      html  css  js  c++  java
  • 14、volatile(轻量级的同步机制)

    引用学习(狂神说)

    谈谈你对 volatile 的理解

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

    它的3个特性:

    1、保证可见性

    2、不保证原子性(原子性就是任务要么完整执行,要么都不执行)

    3、禁止指令重排

    深刻理解volatile的3个特性

    1、保证可见性

    • 上面代码中程序不是死循环了吗?因为线程A并不知道num的值已经被修改。

    • 如何解决呢?

    • 因为volatile关键字保证了可见性,所以主存中的值发生修改后,其他线程可以清晰地看到。

    package com.zxh.testValidate;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo01 {
        // volatile 保证可见性,但主存的值发生修改,线程可以看见
        private volatile static int num = 0; // 内存的变量
    
        public static void main(String[] args) {
            // 线程A不断对num值进行访问
            new Thread(()->{
                while(num == 0){
    
                }
            }).start();
    
            // 为了保证线程A先启动,进行延迟1s,否则会导致num直接=1
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            // main线程在另一线程还在运行时,将num变为1
            num = 1;
            System.out.println(num);    // 输出是否被修改
        }
    }

    2、不保证原子性

    原子性:不可分割

    比如:当线程A在执行时,不能被打扰,要求操作全部都完成,不能被分割。要么同时完成,要么同时失败;

    举例

    • 下面:开了20个线程,每个线程执行了1000次对 num + 1操作。
    package com.zxh.testValidate;
    
    public class Demo02 {
    
        private static int num = 0; // 定义一个变量
    
        public static void add(){
            num++;  // 对num进行+1操作
        }
    
        public static void main(String[] args) {
    
            // 理论情况,num应该变成了20000
            for (int i = 0; i < 20; i++) {
                new Thread(()->{
                    for (int j = 0; j < 1000; j++) {
                        add();
                    }
                }).start();
            }
            // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行
            while(Thread.activeCount() > 2){
                Thread.yield(); // 礼让其他线程先执行    
            }
            // 当其他线程执行完add()操作后,再输出结果
            System.out.println("num:" + num);
    
        }
    
    }

     但是发现 num 并没有加到 20000,为什么呢?

    分析问题的原因

    分析线程的执行操作,解答这个问题

    1、打开生成的 target 目录下的,这个执行文件所在的位置

    2、看到有对应生成的class文件,进入命令行,使用命令javap -c Demo02.class反编译查看

     正如我们所见,add方法执行分成了好几步,所有多个线程操作,可能会插队,比如:导致获取到同一个num=1000 值,而最后两个线程修改的结果为同一个num=1001,就会少增加1

    volatile可以解决吗

    • 答案是不可以,所以volatile有不保证原子性的特点。

    • 现在我们在变量上加入关键字 volatile

     现在我们在变量上加入关键字 volatile

    package com.zxh.testValidate;
    
    public class Demo02 {
        // volatile 不保证原子性
        private volatile static int num = 0; // 定义一个变量
    
        public static void add(){
            num++;  // 对num进行+1操作
        }
    
        public static void main(String[] args) {
    
            // 理论情况,num应该变成了20000
            for (int i = 0; i < 20; i++) {
                new Thread(()->{
                    for (int j = 0; j < 1000; j++) {
                        add();
                    }
                }).start();
            }
            // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行
            while(Thread.activeCount() > 2){
                Thread.yield(); // 礼让其他线程先执行
            }
            // 当其他线程执行完add()操作后,再输出结果
            System.out.println("num:" + num);
    
        }
    
    }

     并不能解决

    问题解决

    到底要如何保证原子性呢?

    首先我们通过反编译class文件知道了,add()方法被拆分成了好几步执行,虽然只有一句num++操作。

    就因为被分成了几个步骤,所以当一个线程执行add()方法的时候,被打扰,也就不能保证原子性的操作,导致出修改的结果重复。

    其他JUC已经提出了解决方法

    查看官方文档

    • 我们已经了解了concurrent和locks包下的大部分类和接口了。

    • 只剩下atomic包,其实这个包就是JUC提供的保证变量原子性的包。 

    • 我们点进去看一下

    • 其实是一些对基本类型封装的类,它们保证了原子

    代码:使用原子类看一下结果

    package com.zxh.testValidate;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class Demo02 {
        // volatile 不保证原子性
        // 使用原子类 保证 原子性
        private volatile static AtomicInteger num = new AtomicInteger(); // 定义一个变量
    
        public static void add(){
            num.getAndIncrement();  // 对num进行+1操作
        }
    
        public static void main(String[] args) {
    
            // 理论情况,num应该变成了20000
            for (int i = 0; i < 20; i++) {
                new Thread(()->{
                    for (int j = 0; j < 1000; j++) {
                        add();
                    }
                }).start();
            }
            // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行
            while(Thread.activeCount() > 2){
                Thread.yield(); // 礼让其他线程先执行
            }
            // 当其他线程执行完add()操作后,再输出结果
            System.out.println("num:" + num);
    
        }
    
    }

    成功解决!但是我们需要知道是怎么解决的,底层是怎么运行的?

    千万不要以为,这个getAndIncrement()方法只是做了+1操作。

    原子类的源码分析

    1、进入这个 + 1方法

    2、发现通过unsafe这个变量进行了 + 1操作

    3、查看这个变量,发现是一个Unsafe

    4、点进去这个类,发现很多方法都是native的本地方法

    所以总结:这些类的底层都直接和操作系统挂钩!直接在内存中修改值!Unsafe类是一个很特殊的存在!

    3、指令重排

    什么是指令重排?

    你写的程序,计算机并不是按照你写的那样去执行的。

    它会通过:源代码-->编译器优化的重排-->指令并行也重排-->内存系统也会重排-->执行

    举个指令重排的栗子

     1、指令重排不会影响结果

    int x = 1;    //1
    int y = 2;    //2    
    x = x + 5;    //3
    y = x * x;    //4
    
    我们所期望的运行顺序:1234
    但是可能执行的时候经过指令重排变成 2134 1324的顺序
    
    有没有可能顺序为:4123!不可能
    因为处理器进行指令重排的时候,会考虑:数据之间的依赖,比如:第3条依赖第1条

    2、指令重排会影响结果

    假设 x y a b默认为0

    线程A线程B
    x=a y=b
    b=1 a=2

    正常的结果:x = 0,y = 0

    但是经过指令重排,因为线程A和线程B里的指令各自没有依赖关系,所以可能变成:

    线程A线程B
    b=1 a=2
    x=a y=b

    指令重排导致诡异的结果:x = 2,y = 1

    volatile为什么可以避免指令重排?

    在内存中会有个内存屏障,阻挡CPU指令。作用:

    1、保证特定的操作的执行顺序!

    2、可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)

    加上volatile,就是在上下加入了屏障

    Volatile 是可以保持 可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!

     

    那个地方会使用volatie?

    那就是下面要讲的单例模式,其中DCL懒汉式用到了

    致力于记录学习过程中的笔记,希望大家有所帮助(*^▽^*)!
  • 相关阅读:
    IE6/IE7浏览器中"float: right"自动换行的解决方法
    IE6/IE7浏览器不支持display: inline-block;的解决方法
    如何解决两个li之间的缝隙
    input、button、a标签 等定义的按钮尺寸的兼容性问题
    在一个页面重复使用一个js函数的方法
    关于让input=text,checkbox居中的解决方法
    遮盖层实现(jQuery+css+html)
    button,input type=button按钮在IE和w3c,firefox浏览器区别
    前端-选项卡(菜单栏)
    形成人、机器、过程和数据的互联互通
  • 原文地址:https://www.cnblogs.com/zxhbk/p/13028037.html
Copyright © 2011-2022 走看看