• final内存语义


    前言

    之前的文章介绍volatile和锁的内存语义,今天我们来介绍下final的内存语义。与前面的锁和volatile相比,对final域的读写更像是普通变量的访问。

    final域的重排序规则

    1.在构造函数内对一个final域得写入,与随后把这个被构造对象得引用赋值给一个引用变量,这两个操作之间不能重排序。
    2.初次读一个包含final域得对象得引用,与之后初次读这个final域,这两个操作之间不能重排序。

    /**
     * 测试fina的重排序规则
     */
    public class FinalExample {
        int i; // 普通变量
        final int j; // final 变量
        static FinalExample obj;
        public FinalExample() {
            i = 1;
            j = 2;
        }
        public static void wirter(){
            obj = new FinalExample();
        }
        public static void reader(){
            FinalExample object = obj;
            int a = object.i;
            int b = object.j;
            System.out.println(a);
            System.out.println(b);
        }
    
        public static void main(String[] args) {
            Thread a = new Thread(new Runnable() {
                @Override
                public void run() {
                    FinalExample.wirter();
                }
            });
    
            Thread b = new Thread(new Runnable() {
                @Override
                public void run() {
                    FinalExample.reader();
                }
            });
            // final一定是在构造函数中加载,不会被重排序,但是普通的就不一定了
            // 可以吧ij在构造函数中,初始化的顺序换一下
            // 就可以知道了
            a.start();
            b.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

    上面的代码,是对这个final重排序的验证,如果按照上面的顺序来的话,输出的结果是1,2,是符合预期的,但是你把构造函数中的i和j的位置呼唤下,这个结果就不会符合预期了。

    写final域的重排序规则

    1.jmm禁止编译器把final域的写重排序到构造函数之外
    2.编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域写重排序到构造函数之外。
    在这里插入图片描述

    读final域的重排序规则

    读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,jmm禁止处理器重排序这两个操作。编译器会在读final域操作前面插入一个LoadLoad屏障。
    在这里插入图片描述

    final域为引用类型

    public class FinalReferenceExample {
        final int[] intArray;
        static FinalReferenceExample obj;
        public FinalReferenceExample() throws InterruptedException {
            intArray = new int[1];
            Thread.sleep(5);
            intArray[0] = 1;
        }
        public static void writerOne() throws InterruptedException {
            obj = new FinalReferenceExample();
        }
        public static void writerTwo() throws InterruptedException {
            Thread.sleep(5);
            obj.intArray[0] = 2;
        }
        public static void reader(){
            if(obj != null){
                int templ = obj.intArray[0];
                System.out.println(templ);
            }
        }
    
        public static void main(String[] args) {
            Thread a = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        FinalReferenceExample.writerOne();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            Thread b = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        FinalReferenceExample.writerTwo();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            Thread c = new Thread(new Runnable() {
                @Override
                public void run() {
                    FinalReferenceExample.reader();
                }
            });
            // 请况很难被模拟出来,所以我就加了这个sleep
            a.start();
            b.start();
            c.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
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    这个很难模拟出出异常的读不到对象引用的情况,但是加上sleep就可以。主要是为了确保第一次读是初始化之前读。
    构造函数内对一个final引用的对象成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

    为什么final引用不能从构造函数溢出

    /**
     * 测试溢出
     */
    public class FinalReferenceEscapeExample {
        final int i;
        static FinalReferenceEscapeExample obj;
        // 构造函数返回之前,被构造对象的引用不能被其他对象可见,不然就会出现溢出
        public FinalReferenceEscapeExample(){
            i = 1;
            obj = this; // 放到前面就不会溢出了,为什么溢出呢,因为指令重排序。这个可能会在构造函数后面执行,类似第一个i一样
        }
        public static void wirter(){
            new FinalReferenceEscapeExample();
        }
    
        public static void reader(){
            if ( obj != null){
                int temp = obj.i;
                System.out.println(temp);
            }
        }
    
        public static void main(String[] args) {
            Thread a = new Thread(new Runnable() {
                @Override
                public void run() {
                    FinalExample.wirter();
                }
            });
    
            Thread b = new Thread(new Runnable() {
                @Override
                public void run() {
                    FinalExample.reader();
                }
            });
            // 异常是对的,i说明没被初始化
            a.start();
            b.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

    为什么不能溢出,就是这个对象的final域一定是在初始化之前完成,而且在final初始化完成之前,这个构造对象是不能被其他线程看见。所以不会导致溢出。之前介绍volatile说到过构造函数的溢出。但是当时没有详细的介绍。这里我就详细的说下,所谓的构造函数的溢出,就是对象初始化完成了,当时构造函数的赋值还没完成。这个时候对象已经不是空的了,但是属性值依然为空,这个时候通过这个对象引用去读这个属性值。就会出现异常或者读出的属性值为空的情况。这个就叫从构造函数中溢出。

    总结

    final的语义大致就是,包含final 的变量和引用的对象,在final变量和引用完成之前,是不能构造出对象,且不能被其他线程所见的。为什么要这样呢,防止fianl的值被改变(如果初始化完成之前你读到fina的int变量可能值为零,初始化完成之后,你再读到可能为1,就不能保证final值不可改变的定义了)。

  • 相关阅读:
    CentOS7多种方式安装MySQL-5.7.X数据库
    人脸识别-Loss-2017:SphereFace【基于angular margin这一流派的人脸识别本质上来说就是基于margin的分类】
    【LLM】Prompt tuning大模型微调实战
    xubuntu16.04系统中隐藏网络连接的弹窗提示
    TS函数与类
    这是不是你们都在找的免费又好用的配音网站?
    读书笔记—《如何阅读一本书》
    深入浅出对话系统——任务型对话系统技术框架
    Node js 开发入门 —UDP 编程,小白也能轻松学会
    DevOps敏捷转型常见误区及避坑指南
  • 原文地址:https://blog.csdn.net/qq_41820066/article/details/127432784