• volatile详解和静态内部类详解


    1.volatile

    volatile是一个特征修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
    volatile是Java虚拟机提供的轻量级的同步机制,它有3个特点:

    1. 保证可见性
    2. 不保证原子性
    3. 禁止指令重排

    1.1 保证可见性

    1.1.1、什么是JMM模型?

    要想理解什么是可见性,首先要先理解JMM。
    JMM(Java内存模型,Java Memory Model)本身是一种抽象的概念,并不真实存在。它描述的是一组规则或规范,通过这组规范,定了程序中各个变量的访问方法。JMM关于同步的规定:

    1. 线程解锁前,必须把共享变量的值刷新回主内存;

    2. 线程加锁前,必须读取主内存的最新值到自己的工作内存;

    3. 加锁解锁是同一把锁;

      由于JVM运行程序的实体是线程,创建每个线程时,JMM会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域。
      Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。
      但线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。
      
      • 1
      • 2
      • 3

    在这里插入图片描述
    如上图所示,所有线程的共享变量都存储在主内存中,每一个线程都有一个独有的工作内存,每个线程不直接操作在主内存中的变量,而是将主内存上变量的副本放进自己的工作内存中,只操作工作内存中的数据。当修改完毕后,再把修改后的结果放回到主内存中。每个线程都只操作自己工作内存中的变量,无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
    上述的Java内存模型在单线程的环境下不会出现问题,但在多线程的环境下可能会出现脏数据,例如:如果有AB两个线程同时拿到变量i,进行递增操作。A线程将变量i放到自己的工作内存中,然后做+1操作,然而此时,线程A还没有将修改后的值刷回到主内存中,而此时线程B也从主内存中拿到修改前的变量i,也进行了一遍+1的操作。最后A和B线程将各自的结果分别刷回到主内存中,看到的结果就是变量i只进行了一遍+1的操作,而实际上A和B进行了两次累加的操作,于是就出现了错误。究其原因,是因为线程B读取到了变量i的脏数据的缘故。
    此时如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。

    1.1.2、代码示例

    1.1.2.1 不加volatile

    package com.xql.designpattern.controller.singleton;
    
    import java.util.concurrent.TimeUnit;
    
    class TestAdd
    {
        int number = 0;
    
        public void add10() {
            this.number += 10;
        }
    }
    
    public class VolatileVisibility {
    
        public static void main(String[] args) {
            TestAdd test = new TestAdd();
    
            // 启动一个线程修改myData的number,将number的值加10
            new Thread(
                    () -> {
                        System.out.println("线程" + Thread.currentThread().getName()+"\t 开始执行");
                        try{
                            TimeUnit.SECONDS.sleep(5);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        test.add10();
                        System.out.println("线程" + Thread.currentThread().getName()+"\t 执行后,number的值为" + test.number);
                    }
            ).start();
    
            // 看一下主线程能否保持可见性
            while (test.number == 0) {
                // 当上面的线程将number加10后,如果有可见性的话,那么就会跳出循环;
                // 如果没有可见性的话,就会一直在循环里执行
            }
    
            System.out.println("===具有可见性!");
        }
    }
    
    
    • 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

    执行结果:发现卡死在while 主线程没收到通知
    在这里插入图片描述

    1.1.2.2 加volatile

    在这里插入图片描述

    package com.xql.designpattern.controller.singleton;
    
    import java.util.concurrent.TimeUnit;
    
    class TestAdd
    {
        volatile int number = 0;
    
        public void add10() {
            this.number += 10;
        }
    }
    
    public class VolatileVisibility {
    
        public static void main(String[] args) {
            TestAdd test = new TestAdd();
    
            // 启动一个线程修改myData的number,将number的值加10
            new Thread(
                    () -> {
                        System.out.println("线程" + Thread.currentThread().getName()+"\t 开始执行");
                        try{
                            TimeUnit.SECONDS.sleep(5);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        test.add10();
                        System.out.println("线程" + Thread.currentThread().getName()+"\t 执行后,number的值为" + test.number);
                    }
            ).start();
    
            // 看一下主线程能否保持可见性
            while (test.number == 0) {
                // 当上面的线程将number加10后,如果有可见性的话,那么就会跳出循环;
                // 如果没有可见性的话,就会一直在循环里执行
            }
    
            System.out.println("===具有可见性!");
        }
    }
    
    
    • 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

    执行结果:跳出了while循环 主线程继续执行了 说明收到了通知 具有可见性
    在这里插入图片描述

    • 小结:
      JMM内存模型的可见性是指,多线程访问主内存的某一个资源时,如果某一个线程在自己的工作内存中修改了该资源,并写回主内存,那么JMM内存模型应该要通知其他线程来从新获取最新的资源,来保证最新资源的可见性。

    1.2 volatile不保证原子性

    需要重点说明的一点是,尽管volatile关键字可以保证内存可见性和有序性,但不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。请看以下的例子:

    1.2.1、代码示例

    package com.xql.designpattern.controller.singleton;
    
    import java.util.concurrent.TimeUnit;
    
    class TestAdd
    {
        volatile int number = 0;
    
        public void add10() {
            this.number += 10;
        }
    
        public void add() {
            number++;
        }
    }
    
    public class VolatileVisibility {
    
        public static void main(String[] args) {
            TestAdd test = new TestAdd();
    
    
            // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
            for (int i=0; i<20; i++) {
                new Thread(() -> {
                    for (int j=0; j<1000; j++) {
                        test.add();
                    }
                }).start();
            }
    
            // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
            while (Thread.activeCount()>2){
                Thread.yield();
            }
    
            System.out.println("number值加了20000次,此时number的实际值是:" + test.number);
    
        }
    }
    
    
    • 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

    执行结果:
    在这里插入图片描述
    变量number被volatile所修饰,并启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000,但实际的情况是,每次运行结果可能都是一个小于20000的数字(也有结果为20000的时候,但出现几率很小),并且不固定。那么这是为什么呢?
    原因是因为“number++;”这行代码并不是原子操作,尽管它被volatile所修饰了也依然如此。++操作的执行过程如下面所示:

    1. 首先获取变量i的值
    2. 将该变量的值+1
    3. 将该变量的值写回到对应的主内存中
      在这里插入图片描述

    1.2.2 解决方法:

    1.可以加synchronized
    在这里插入图片描述
    2.JUC包下的原子类AtomicInteger

        volatile AtomicInteger number = new AtomicInteger(0);
    
        public  void add() {
            number.getAndIncrement();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.3 volatile禁止指令重排

    计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排:
    源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令

    处理器在进行重排时,必须要考虑指令之间的数据依赖性。
    单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。
    但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测。

    1.3.2 示例

    看了上面的文字性表达,然后看一个很简单的例子。
    比如下面的mySort方法,在系统指令重排后,可能存在以下3种语句的执行情况:
    1)1234
    2)2134
    3)1324
    以上这3种重排结果,对最后程序的结果都不会有影响,也考虑了指令之间的数据依赖性。

    public void mySort() {
        int x = 1;  // 语句1
        int y = 2;  // 语句2
        x = x + 3;  // 语句3
        y = x * x;  // 语句4
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    JVM会保证在单线程的情况下,重排序后的执行结果会和重排序之前的结果一致。但是在多线程的场景下就不一定了。最典型的例子就是双重检查加锁版的单例实现,代码如下所示:

    public class Singleton {
     
        private volatile static Singleton instance;
     
        private Singleton() {}
     
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    由上可以看到,instance变量被volatile关键字所修饰,但是如果去掉该关键字,就不能保证该代码执行的正确性。这是因为“instance = new Singleton();”这行代码并不是原子操作,其在JVM中被分为如下三个阶段执行:

    1. 为instance分配内存
    2. 初始化instance
    3. 将instance变量指向分配的内存空间
      由于JVM可能存在重排序,上述的二三步骤没有依赖的关系,可能会出现先执行第三步,后执行第二步的情况。也就是说可能会出现instance变量还没初始化完成,其他线程就已经判断了该变量值不为null,结果返回了一个没有初始化完成的半成品的情况。而加上volatile关键字修饰后,可以保证instance变量的操作不会被JVM所重排序,每个线程都是按照上述一二三的步骤顺序的执行,这样就不会出现问题。

    1.3.2 内存屏障

        volatile有序性是通过内存屏障实现的。JVM和CPU都会对指令做重排优化,所以在指令间插入一个屏障点,就告诉JVM和CPU,不能进行重排优化。具体的会分为读读、读写、写读、写写屏障这四种,同时它也会有一些插入屏障点的策略,下面是JMM基于保守策略的内存屏障点插入策略:
    
    • 1

    在这里插入图片描述

    1.3.3 volatile能保证禁止指令重排的原理

    在这里插入图片描述

    2.静态内部类

    2.1静态内部类概念

    使用静态的类的只有一种情况,就是在内部类中。如果是在外部类中使用static关键字是会报错的。

    2.2静态内部类特点

    1. 静态内部类可以在外部类的静态成员中访问或者实例化(非静态内部类不可以)—优势
    2. 静态内部类可以访问外部类的静态成员不可以访问非静态成员(非静态内部类可以访问类的静态和非静态成员)—限制
    3. 静态内部类可以申明静态成员(非静态内部类则不可以申明静态成员)
    4. 在外部类外创建该外部类的内部静态类,不用依附于外部类的实例(而非静态内部类则需要依赖于外部类的实例)

    2.3静态内部类的加载时机

    静态内部类的加载时机?他和外部类的加载有没有什么关系?

    静态内部类的加载是在程序中调用静态内部类的时候加载的,和外部类的加载没有必然关系,但是在加载静态内部类的时候 发现外部类还没有加载,那么就会先加载外部类,加载完外部类之后,再加载静态内部类(初始化静态变量和静态代码块etc)如果在程序中单纯的使用 外部类,并不会触发静态内部类的加载
    扩展:
    ①一个类内部有静态内部类和非静态内部类 , 静态内部类和非静态内部类一样,都是在被调用时才会被加载
    不过在加载静态内部类的过程中如果没有加载外部类,也会加载外部类
    静态变量,静态方法,静态块等都是类级别的属性,而不是单纯的对象属性。
    他们在类第一次被使用时被加载 (记住,是一次使用,不一定是实例化)
    我们可以简单得用 类名.变量 或者 类名.方法来调用它们
    与调用没有被static 修饰过变量和方法不同的是:一般变量和方法是用当前对象的引用(即this)来调用的, 静态的方法和变量则不需要。从一个角度上来说,它们是共享给所有对象的,不是一个角度私有。 这点上,静态内部类也是一样的。
    ② 类的加载时机:(暂时的认知里是四种) new 一个类的时候,调用类内部的 静态变量,调用类的静态方法,调用类的 静态内部类

    2.4代码示例

    package com.xql.designpattern.controller.singleton;
     
    import lombok.SneakyThrows;
     
     
    public class OuterClass {
        public static String OUTER_DATE = "外部类静态变量加载时间 "+System.currentTimeMillis();
        static {
            System.out.println("外部类静态块加载时间:" + System.currentTimeMillis());
        }
        public OuterClass() {
            System.out.println("外部类构造函数时间:" + System.currentTimeMillis());
        }
     
        static class InnerStaticClass{
            public static String INNER_STATIC_DATE = "静态内部类静态变量加载时间 "+System.currentTimeMillis();
            static {
                System.out.println("静态内部类静态代码块加载时间:" + System.currentTimeMillis());
            }
        }
        class InnerClass {
            public String INNER_DATE = "";
            public InnerClass() {
                INNER_DATE = "非静态内部类构造器加载时间"+System.currentTimeMillis();
            }
        }
     
     
        @SneakyThrows
        public static void main(String[] args) {
            //①main方法里没有任何代码运行结果
            //  外部类静态块加载时间:1614393999819
     
            //  说明:外部类静态变量的加载时间和外部类静态代码块的加载时间一样
     
            // ②
             //OuterClass outer = new OuterClass();
             //外部类静态块加载时间:1614394114095
             //外部类构造函数时间:1614394114095
     
            // 说明加载外部类的时候并没有加载静态内部类,外部类静态变量的加载时间和外部类静态代码块的加载时间一样
     
     
            // ③
    //        OuterClass outer = new OuterClass();
    //        Thread.sleep(10000L);
    //        System.out.println("外部类静态变量加载时间:" + outer.OUTER_DATE);
     
            //外部类静态块加载时间:1614394454245
            //外部类构造函数时间:1614394454245
            //外部类静态变量加载时间:外部类静态变量加载时间 1614394454245
     
            // 说明:加载外部类和加载静态内部类没有什么关系,外部类是程序调用外部类的的时候会加载
     
     
     
            //④
    //        OuterClass outer = new OuterClass();
    //        Thread.sleep(10000L);
    //        System.out.println("非静态内部类加载时间: "+outer.new InnerClass().INNER_DATE);
            //外部类静态块加载时间:1614394800484
            //外部类构造函数时间:1614394800484
            //非静态内部类加载时间: 非静态内部类构造器加载时间614394810501
     
     
     
            // ⑤(ps) 内部静态类可以直接用,不需要new
            //System.out.println("静态内部类加载时间____:"+InnerStaticClass.INNER_STATIC_DATE);
            //外部类静态块加载时间:1614395200427
            //静态内部类静态代码块加载时间:1614395200430
            //静态内部类加载时间____:静态内部类静态变量加载时间 1614395200430
     
            //说明:静态内部类的加载是代码中需要静态内部类的时候才加载,而不是和外部类一起加载的
            // 加载静态内部类之前,先把外部类的静态变量和静态代码块先执行完
            // 执行完外部类的代码后,再执行静态内部类的 静态变量和静态代码块
            // 静态内部类的 静态变量和静态代码块执行完后,然后执行业务代码(⑤ 中的打印语句)
     
     
            //⑥ 验证如果加载过了外部类,调用静态内部类不需要重新加载外部类
    //        OuterClass outer = new OuterClass();
    //        Thread.sleep(10000L);
    //        System.out.println("静态内部类加载时间____:"+InnerStaticClass.INNER_STATIC_DATE);
            // 外部类静态块加载时间:1614395065015
            //外部类构造函数时间:1614395065015
            //静态内部类静态代码块加载时间:1614395075029
            //静态内部类加载时间____:静态内部类静态变量加载时间 1614395075029
     
            // 说明:new  外部类的时候 。外部类的静态代码块和静态变量先执行,外部类构造函数后执行
     
     
     
        }
     
    }
    
    • 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
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
  • 相关阅读:
    SpringMvc高级(拦截器和文件上传下载)
    Django学习笔记二:数据库配置
    2.学习Vue入门知识点
    Postgresql垃圾回收Vacuum优化手册
    深入理解JVM虚拟机第十二篇:JVM中的线程说明
    Deformable Convolutional Networks
    第二十二次CCF计算机软件能力认证
    【Python 实战基础】Pandas如何精确设置表格数据的单元格的值
    Echarts散点图筛选新玩法dataZoom
    【Matplotlib绘制图像大全】(七):Matplotlib使用xlim()和ylim()修改轴线刻度
  • 原文地址:https://blog.csdn.net/qq_42264638/article/details/126387714