Java并发机制底层实现原理之final

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

一、final基础使用

1.1、修饰类

当某个类定义为final时,即这个类是不能有子类,final类中的所有方法都隐式为final,无法覆盖它们,所以在final类中给任何方法添加关键字是没有任何意义的。

final类型的类如何拓展?
设计模式中最重要的两种关系,一种是继承/实现;另外一种是组合关系。所以当遇到不能继承的(final修饰的类),考虑用组合。
复制代码

1.2、修饰方法
  • private方法是隐式的final
  • final方法是可以被重载的


private final:类中所有private方法都隐式地指定为final的,由于无法取用private方法,所以也就不能覆盖它,对private方法增添final关键字,这样做并没有什么好处。

final方法可以被重载:父类的final方法是不能够被子类重写的,那么final方法可以被重载?可以的


1.3、修饰参数

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


1.4、修饰变量

所有的final修饰的字段都是编译期常量吗?

public class Test {
    //编译期常量
    final int i = 1;
    final static int J = 1;
    final int[] a = {1,2,3,4};
    //非编译期常量
    Random r = new Random();
    final int k = r.nextInt();

    public static void main(String[] args) {

    }
}
复制代码

k的值由随机数对象决定,所以不是所有的final修饰的字段都是编译期常量,只是k的值在被始化后无法被更改。

1.5、static final

一个既是static又是final的字段只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过。

import java.util.Random;
public class Test {
    static Random r = new Random();
    final int k = r.nextInt(10);
    static final int k2 = r.nextInt(10); 
    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("k="+t1.k+" k2="+t1.k2);
        Test t2 = new Test();
        System.out.println("k="+t2.k+" k2="+t2.k2);
    }
}
复制代码

执行结果输出:

扫描二维码关注公众号,回复: 13348669 查看本文章
k=2 k2=7
k=8 k2=7
复制代码

static关键字所修饰的字段并不属于一个对象,而是属于这个类的,也可以理解为static final所修饰的字段仅占据内存的一个一份空间,一旦被初始化之后便不会被更改。

二、final域重排序规则

2.1、final域为基本类型
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域
    }
}
复制代码

假设线程A在执行writer()方法,线程B执行reader()方法。

写final域重排序规则:

写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含两个方面:
1、JMM禁止编译器把final域的写重排序到构造函数之外;
2、编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

分析writer方法:
1、构造了一个FinalDemo对象;
2、把这个对象赋值给成员变量finalDemo.

image.png

image.png


由于a、b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(0),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。

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

读final域重排序规则:

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

read()方法主要包含三个操作:
1、初次读引用变量finalDemo;
2、初次读引用变量finalDemo的普通域a;
3、初次读引用变量finalDemo的final域b;

image.png

image.png


读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显示是错误的操作。而final域的读操作就限定了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。

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


2.2、final域为引用类型


对final修饰的对象的成员域写操作:

针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。

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执行writerOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法\

image.png

image.png


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

对final修饰的对象的成员域读操作

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

猜你喜欢

转载自juejin.im/post/7034430703399862286