final 这个平时经常在工具类中用到的关键字,通常知道的是它可以用来修饰类、方法、变量,修饰后表示「不可变」。但除此之外,final 经常在多线程并发场景下被提及,java 编程语言规范中也在 17 章 Thread and Locks 中提及到了 final,所以 final 在多线程的场景下有些什么特点值得深思
基础使用 快速过下 final 修饰类、方法、变量的特点
修饰类 当某个类被 final 时,这个类无法被继承,即这个类是不能有子类的
final 类中的所有方法都隐式为 final,因为无法覆盖他们,所以在 final 类中给任何方法添加 final 关键字是没有任何意义的
不过虽然无法被继承,但可以通过组合的方式来拓展这个类
1 2 3 4 5 6 7 8 9 10 11 12 13 class MyString { private String innnerString; public int length () { return innerString.length(); } public void m1 () { ... } }
修饰方法 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 public static void main (String[] args) { C2 c2 = new C2 (); c2.m1(); c2.m2(); C1 c1 = c2; c1.m1(); c1.m2(); } private static class C1 { private void m1 () { System.out.println("c1 m1" ); } public void m2 () { System.out.println("c1 m2" ); } } private static class C2 extends C1 { private void m1 () { System.out.println("c2 m1" ); } public void m2 () { System.out.println("c2 m2" ); } }
Out:
两个特点:
private 修饰的方法是隐式的 final
final 方法不能被覆盖,但是能被重载
修饰变量 1 2 3 4 5 6 7 8 9 10 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(); }
static final 修饰的变量声明即刻赋值
final 变量可以在构造函数内赋值
final 域重排序规则 编译器、处理器和 JIT 编译器的指令重排序优化可能会导致多线程下的有序性问题,而由于 final 的不可更改性,因此编译器会针对 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 24 25 26 27 28 29 class FinalTest2 { final int x; int y; static FinalTest2 f; public FinalTest2 () { x = 3 ; y = 4 ; } static void writer () { f = new FinalTest2 (); } static void reader () { if (f == null ) { System.out.println("f is null!" ); } else { System.out.println("f.y: " + f.y); System.out.println("f.x: " + f.x); } } public static void main (String[] args) { new Thread (FinalTest2::writer).start(); new Thread (FinalTest2::reader).start(); } }
由于 x,y 之间没有依赖性,普通变量 y 可能会被重排序到构造函数之外,这样 reader 线程读到的 y 可能是一个还未完成赋值的默认值 0
final 引用类型 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 class FinalTest3 { final int [] arrays; FinalTest3 f; public FinalTest3 () { arrays = new int [1 ]; arrays[0 ] = 1 ; } void write () { f = new FinalTest3 (); } void read () { if (f == null ) { System.out.println("f is null" ); } else { System.out.println("f's arrays is: " + f.arrays[0 ]); } } public static void main (String[] args) { FinalTest3 test3 = new FinalTest3 (); new Thread (test3::write).start(); new Thread (test3::read).start(); } }
f.arrays[0]
输出的结果总是 0,说明 array[0] = 1
无法从构造函数中逃逸出来
重排序原理 由于 final 的不可变特性,如果 final 变量在构造函数中初始化,使得 JVM 会在构造函数返回前加入 StoreStore 屏障,其他线程在读取 final 变量前会加入 LoadLoad 屏障,屏障保证了 final 变量的有序性与可见性,即保证每个线程看到的值都会是相同的
引用类型会额外保证构造函数内对 final 对象写入的有序性与可见性
this 溢出 final 可以保证在使用一个 final 对象的引用时,该对象已被构造函数初始化完成过
但需要 避免在构造过程中泄露 this ,即不要在构造函数中引用 this,这可能导致其他线程访问到初始化部分的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class FinalReferenceEscapeDemo { private final int a; private FinalReferenceEscapeDemo referenceDemo; public FinalReferenceEscapeDemo () { a = 1 ; referenceDemo = this ; } public void writer () { new FinalReferenceEscapeDemo (); } public void reader () { if (referenceDemo != null ) { int temp = referenceDemo.a; } } }
假设一个线程 A 执行 writer 方法另一个线程 B 执行 reader 方法。因为构造函数中操作 1 和 2 之间没有数据依赖性,1 和 2 可以重排序,先执行了 2,这个时候引用对象 referenceDemo 是个没有完全初始化的对象,而当线程 B 去读取该对象时就会出错。尽管依然满足了 final 域写重排序规则:在引用对象对所有线程可见时,其 final 域已经完全初始化成功。但是,引用对象 this 逸出,该代码依然存在线程安全的问题
String 线程安全吗 String 是线程安全的
既然如此
1 2 3 4 5 6 7 8 9 10 static String s = "1" ;public static void main (String[] args) { for (int i = 0 ; i < 10 ; i++) { new Thread (() -> { s += "1" ; }).start(); } System.out.println(s); }
String 依旧是线程安全的,只不过这里并没有用 String,而是创建了一堆 String