Final关键字

final的功能

final关键字可以用于多个场景,且在不同场景中具有不同的作用。

  1. 修饰类

    当某个类的整体定义为final时, 这个类禁止被继承。 因此final类中的所有方法都隐式为final,因为无法覆盖他们,所以在final类中给任何方法添加final关键字是没有任何意义的。

  2. 修饰方法

    • 所有的private方法都是隐式的final,因为 由于无法取用private方法,所以也就不能覆盖它。可以对private方法增添final关键字,但这样做并没有什么好处。
    • final方法可以重载
  3. 修饰参数

    Java允许在参数列表中以声明的方式将参数指明为final,这意味这你无法在方法中更改参数引用所指向的对象。这个特性主要用来向匿名内部类传递数据。

  4. 修饰变量

    • final可以修饰非编译器常量,即final int a = varb
    • static final: 一个既是static又是final 的字段只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过。
    • 空白final: 也就是说被声明为final但又没有给出定值的字段,但是必须在该字段被使用之前被赋值(在构造函数中赋值)。
    • final修饰引用对象时,引用不能变,但是被引用对象的内容可变。
    • final修饰基本类型时,看做常量,存于常量池中

final域重排序规则

final域为基本类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FinalDemo {
private int a; //普通域
private final int b; //final域
private static FinalDemo finalDemo;

public FinalDemo() {
a = 1; // 1. 写普通域
b = 2; // 2. 写final域
}

public static void writer() {
finalDemo = new FinalDemo();
}

public static void reader() {
FinalDemo demo = finalDemo; // 3.读对象引用
int a = demo.a; //4.读普通域
int b = demo.b; //5.读final域
}
}
  1. 写final域重排序规则

    写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:

    • JMM禁止编译器把final域的写重排序到构造函数之外;
    • 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外, 同时保证前面的对final写入对其他线程/CPU可见 。

    因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。

  2. 读final域重排序规则

    读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。

    总之, 读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。

final域为引用类型

  1. 写final域重排序规则

    在基本类型的基础上增加了一条: 在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class FinalReferenceDemo {
    final int[] arrays;
    private FinalReferenceDemo finalReferenceDemo;

    public FinalReferenceDemo() {
    arrays = new int[1]; //1
    arrays[0] = 1; //2
    }

    public void writerOne() {
    finalReferenceDemo = new FinalReferenceDemo(); //3
    }

    public void writerTwo() {
    arrays[0] = 2; //4
    }

    public void reader() {
    if (finalReferenceDemo != null) { //5
    int temp = finalReferenceDemo.arrays[0]; //6
    }
    }
    }

    针对上面的实例程序,线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法。

    由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。

  2. 读final域重排序规则

    JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信