关于java:从菜鸟程序员到高级架构师竟然是因为这个字final

4次阅读

共计 5741 个字符,预计需要花费 15 分钟才能阅读完成。

final 实现原理

简介

final关键字,理论的含意就一句话,不可扭转。什么是不可扭转?就是初始化实现之后就不能再做任何的批改,润饰成员变量的时候,成员变量变成一个常数;润饰办法的时候,办法不容许被重写;润饰类的时候,类不容许被继承;润饰参数列表的时候,入参的对象也是不能够扭转。这个就是不可变,无论是援用新的对象,重写还是继承,都是扭转的办法,而 final 就是把这个变更的路给堵死

用法

final 润饰变量

  • final 成员变量示意常量,只能被赋值一次,赋值后值不再扭转(final 要求地址值不能扭转)
  • 当 final 润饰一个根本数据类型时,示意该根本数据类型的值一旦在初始化后便不能发生变化;
  • 如果 final 润饰一个援用类型时,则在对其初始化之后便不能再让其指向其余对象了,但该援用所指向的对象的内容是能够发生变化的。实质上是一回事,因为援用的值是一个地址,final 要求值,即地址的值不发生变化。
  • final 润饰一个成员变量(属性),必须要显示初始化。这里有两种初始化形式。

    • 一种是在变量申明的时候初始化。
    • 第二种办法是在申明变量的时候不赋初值,然而要在这个变量所在的类的所有的构造函数中对这个变量赋初值。

final 润饰办法

应用 final 办法的起因有两个。

  • 第一个起因是把办法锁定,以防任何继承类批改它的含意,不能被重写;
  • 第二个起因是效率,final 办法比非 final 办法要快,因为在编译的时候曾经动态绑定了,不须要在运行时再动静绑定。

注:类的 private 办法会隐式地被指定为 final 办法

final 润饰类

当用 final 润饰一个类时,表明这个类不能被继承。

final 类中的成员变量能够依据须要设为 final,然而要留神 final 类中的所有成员办法都会被隐式地指定为 final 办法。

在应用 final 润饰类的时候,要留神审慎抉择,除非这个类真的在当前不会用来继承或者出于平安的思考,尽量不要将类设计为 final 类。

final 关键字的益处

  • final 关键字进步了性能。JVM 和 Java 利用都会缓存 final 变量。
  • final 变量能够平安的在多线程环境下进行共享,而不须要额定的同步开销。
  • 应用 final 关键字,JVM 会对办法、变量及类进行优化。

注意事项

  • final 关键字能够用于成员变量、本地变量、办法以及类。
  • final 成员变量必须在申明的时候初始化或者在结构器中初始化,否则就会报编译谬误。
  • 你不可能对 final 变量再次赋值。
  • 本地变量必须在申明时赋值。
  • 在匿名类中所有变量都必须是 final 变量。
  • final 办法不能被重写。
  • final 类不能被继承。
  • final 关键字不同于 finally 关键字,后者用于异样解决。
  • final 关键字容易与 finalize()办法搞混,后者是在 Object 类中定义的办法,是在垃圾回收之前被 JVM 调用的办法。
  • 接口中申明的所有变量自身是 final 的。
  • final 和 abstract 这两个关键字是反相干的,final 类就不可能是 abstract 的。
  • final 办法在编译阶段绑定,称为动态绑定(static binding)。
  • 没有在申明时初始化 final 变量的称为空白 final 变量 (blank final variable),它们必须在结构器中初始化,或者调用 this() 初始化。不这么做的话,编译器会报错“final 变量 (变量名) 须要进行初始化”。
  • 将类、办法、变量申明为 final 可能进步性能,这样 JVM 就有机会进行预计,而后优化。
  • 依照 Java 代码常规,final 变量就是常量,而且通常常量名要大写。
  • 对于汇合对象申明为 final 指的是援用不能被更改,然而你能够向其中减少,删除或者扭转内容。

原理

内存语义

写内存语义能够确保在对象的援用为任意线程可见之前,final 域曾经被初始化过了。

读内存语义能够确保如果对象的援用不为 null,则阐明 final 域曾经被初始化过了。

总之,final 域的内存语义提供了初始化平安保障。

  • 写内存语义:在构造函数内对一个 final 域的写入,与随后将对象援用赋值给援用变量,这两个操作不能重排序。
  • 读内存语义:首次读一个蕴含 final 域的对象的援用,与随后首次读这个 final 域,这两个操作不能重排序。

写 final 域的重排序规定

写 final 域的重排序规定禁止把 final 域的写重排序到构造函数之外。这个规定的实现蕴含上面 2 个方面:

  • JMM 禁止编译器把 final 域的写重排序到构造函数之外。
  • 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。

当初让咱们剖析 writer () 办法。writer () 办法只蕴含一行代码:finalExample = new FinalExample ()。这行代码蕴含两个步骤:

  1. 结构一个 FinalExample 类型的对象;
  2. 把这个对象的援用赋值给援用变量 obj。

假如线程 B 读对象援用与读对象的成员域之间没有重排序(马上会阐明为什么须要这个假如),下图是一种可能的执行时序:

在上图中,写一般域的操作被编译器重排序到了构造函数之外,读线程 B 谬误的读取了一般变量 i 初始化之前的值。而写 final 域的操作,被写 final 域的重排序规定“限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。

写 final 域的重排序规定能够确保:在对象援用为任意线程可见之前,对象的 final 域曾经被正确初始化过了,而一般域不具备这个保障。以上图为例,在读线程 B“看到”对象援用 obj 时,很可能 obj 对象还没有结构实现(对一般域 i 的写操作被重排序到构造函数外,此时初始值 2 还没有写入一般域 i)。

读 final 域的重排序规定

读 final 域的重排序规定如下:

在一个线程中,首次读对象援用与首次读该对象蕴含的 final 域,JMM 禁止处理器重排序这两个操作(留神,这个规定仅仅针对处理器)。编译器会在读 final 域操作的后面插入一个 LoadLoad 屏障。

首次读对象援用与首次读该对象蕴含的 final 域,这两个操作之间存在间接依赖关系。因为编译器恪守间接依赖关系,因而编译器不会重排序这两个操作。大多数处理器也会恪守间接依赖,大多数处理器也不会重排序这两个操作。但有多数处理器容许对存在间接依赖关系的操作做重排序(比方 alpha 处理器),这个规定就是专门用来针对这种处理器。

reader() 办法蕴含三个操作:

  1. 首次读援用变量 obj;
  2. 首次读援用变量 obj 指向对象的一般域 j。
  3. 首次读援用变量 obj 指向对象的 final 域 i

当初咱们假如写线程 A 没有产生任何重排序,同时程序在不恪守间接依赖的处理器上执行,上面是一种可能的执行时序

在上图中,读对象的一般域的操作被处理器重排序到读对象援用之前。读一般域时,该域还没有被写线程 A 写入,这是一个谬误的读取操作。而读 final 域的重排序规定会把读对象 final 域的操作“限定”在读对象援用之后,此时该 final 域曾经被 A 线程初始化过了,这是一个正确的读取操作。

读 final 域的重排序规定能够确保:在读一个对象的 final 域之前,肯定会先读蕴含这个 final 域的对象的援用。在这个示例程序中,如果该援用不为 null,那么援用对象的 final 域肯定曾经被 A 线程初始化过了。

如果 final 域是援用类型

下面咱们看到的 final 域是根底数据类型,上面让咱们看看如果 final 域是援用类型,将会有什么成果?

请看下列示例代码:

COPYpublic class FinalReferenceExample {final int[] intArray;                     //final 是援用类型 
    static FinalReferenceExample obj;

    public FinalReferenceExample () {        // 构造函数 
        intArray = new int[1];              //1
        intArray[0] = 1;                   //2
    }

    public static void writerOne () {          // 写线程 A 执行 
        obj = new FinalReferenceExample ();  //3}

    public static void writerTwo () {          // 写线程 B 执行 
        obj.intArray[0] = 2;                 //4
    }

    public static void reader () {              // 读线程 C 执行 
        if (obj != null) {                    //5
            int temp1 = obj.intArray[0];       //6
        }
    }
}

这里 final 域为一个援用类型,它援用一个 int 型的数组对象。对于援用类型,写 final 域的重排序规定对编译器和处理器减少了如下束缚:

在构造函数内对一个 final 援用的对象的成员域的写入,与随后在构造函数外把这个被结构对象的援用赋值给一个援用变量,这两个操作之间不能重排序。

对下面的示例程序,咱们假如首先线程 A 执行 writerOne() 办法,执行完后线程 B 执行 writerTwo() 办法,执行完后线程 C 执行 reader () 办法。上面是一种可能的线程执行时序:

在上图中,1 是对 final 域的写入,2 是对这个 final 域援用的对象的成员域的写入,3 是把被结构的对象的援用赋值给某个援用变量。这里除了后面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。

JMM 能够确保读线程 C 至多能看到写线程 A 在构造函数中对 final 援用对象的成员域的写入。即 C 至多能看到数组下标 0 的值为 1。而写线程 B 对数组元素的写入,读线程 C 可能看的到,也可能看不到。JMM 不保障线程 B 的写入对读线程 C 可见,因为写线程 B 和读线程 C 之间存在数据竞争,此时的执行后果不可预知。

如果想要确保读线程 C 看到写线程 B 对数组元素的写入,写线程 B 和读线程 C 之间须要应用同步原语(lock 或 volatile)来确保内存可见性。

为什么 final 援用不能从构造函数内“逸出”

后面咱们提到过,写 final 域的重排序规定能够确保:在援用变量为任意线程可见之前,该援用变量指向的对象的 final 域曾经在构造函数中被正确初始化过了。其实要失去这个成果,还须要一个保障:在构造函数外部,不能让这个被结构对象的援用为其余线程可见,也就是对象援用不能在构造函数中“逸出”。为了阐明问题,让咱们来看上面示例代码:

COPYpublic class FinalReferenceEscapeExample {
    final int i;
    static FinalReferenceEscapeExample obj;

    public FinalReferenceEscapeExample () {
        i = 1;                              //1 写 final 域 
        obj = this;                          //2 this 援用在此“逸出”}

    public static void writer() {new FinalReferenceEscapeExample ();
    }    

    public static void reader {if (obj != null) {                     //3
            int temp = obj.i;                 //4
        }
    }
}

假如一个线程 A 执行 writer() 办法,另一个线程 B 执行 reader() 办法。这里的操作 2 使得对象还未实现结构前就为线程 B 可见。即便这里的操作 2 是构造函数的最初一步,且即便在程序中操作 2 排在操作 1 前面,执行 read() 办法的线程依然可能无奈看到 final 域被初始化后的值,因为这里的操作 1 和操作 2 之间可能被重排序。理论的执行时序可能如下图所示:

从上图咱们能够看出:在构造函数返回前,被结构对象的援用不能为其余线程可见,因为此时的 final 域可能还没有被初始化。在构造函数返回后,任意线程都将保障能看到 final 域正确初始化之后的值。

final 语义在处理器中的实现

当初咱们以 x86 处理器为例,阐明 final 语义在处理器中的具体实现。

下面咱们提到,写 final 域的重排序规定会要求译编器在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 障屏。读 final 域的重排序规定要求编译器在读 final 域的操作后面插入一个 LoadLoad 屏障。

因为 x86 处理器不会对写 – 写操作做重排序,所以在 x86 处理器中,写 final 域须要的 StoreStore 障屏会被省略掉。同样,因为 x86 处理器不会对存在间接依赖关系的操作做重排序,所以在 x86 处理器中,读 final 域须要的 LoadLoad 屏障也会被省略掉。也就是说在 x86 处理器中,final 域的读 / 写不会插入任何内存屏障!

为什么要加强 final 的语义

在旧的 Java 内存模型中,最重大的一个缺点就是线程可能看到 final 域的值会扭转。比方,一个线程以后看到一个整形 final 域的值为 0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个 final 域的值时,却发现值变为了 1(被某个线程初始化之后的值)。最常见的例子就是在旧的 Java 内存模型中,String 的值可能会扭转。

为了修补这个破绽,JSR-133 专家组加强了 final 的语义。通过为 final 域减少写和读重排序规定,能够为 java 程序员提供初始化平安保障:只有对象是正确结构的(被结构对象的援用在构造函数中没有“逸出”),那么不须要应用同步(指 lock 和 volatile 的应用),就能够保障任意线程都能看到这个 final 域在构造函数中被初始化之后的值。

final、finally、finalize 区别

  • final 能够用来润饰类、办法、变量,别离有不同的意义,final 润饰的 class 代表不能够继承扩大,final 的变量是不能够批改的,而 final 的办法也是不能够重写的(override)。
  • finally 则是 Java 保障重点代码肯定要被执行的一种机制。咱们能够应用 try-finally 或者 try-catch-finally 来进行相似敞开 JDBC 连贯、保障 unlock 锁等动作。
  • finalize 是根底类 java.lang.Object 的一个办法,它的设计目标是保障对象在被垃圾收集前实现特定资源的回收。finalize 机制当初曾经不举荐应用,并且在 JDK 9 开始被标记为 deprecated。

本文由 传智教育博学谷狂野架构师 教研团队公布。

如果本文对您有帮忙,欢送 关注 点赞 ;如果您有任何倡议也可 留言评论 私信,您的反对是我保持创作的能源。

转载请注明出处!

正文完
 0