You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
android-notes/blogs/Java/final.md

4.9 KiB

final

目录

  1. final
    • 引言
    • 内存语义
    • 优势所在
      • 天生线程安全
      • 编译器优化
    • 参考

引言

说起 final 关键字,脱口而出就是修饰变量变量不可变、修饰方法方法不可重写、修饰类类不可继承。

或许你还听过 final 能够提高效率,那如何提高效率的呢?方法内联?方法内联又是个什么东西呢?

带着疑问来探索一下 final。

内存语义

写 final 域的重排序规则

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

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

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

读 final 域的重排序规则

在一个线程中,初次读对象引用与初次读改对象包含的 final 域,JMM 禁止处理器重排序这两个操作。编译器会在读 final 域操作的前面插入一个 LoadLoad 内存屏障。

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

优势

线程安全

这个不用多说,不可变对象天生线程安全。

编译器优化

经常能听到别人说 final 能提高效率,在于编译器在对 final 修饰的方法做了方法内联。这句话对与否先不讨论,先来了解一下方法内联是什么鬼?

在了解方法内联之前,先来了解一下函数调用过程:

  1. 首先会有个执行栈,存储它们的局部变量、方法名、动态链接地址
  2. 当一个方法调用,一个新的栈桢会被加到栈顶,分配的本地变量和参数会存储在这个栈桢
  3. 调转到目标方法代码执行
  4. 方法返回的时候,本地方法和参数被销毁,栈顶被移除
  5. 返回原来的地址执行

其实这就是虚拟机栈执行方法的内存模型。函数调用需要一定的时间开销和空间开销,当一个方法被频繁调用时,这个时间和空间开销会相对变的很大。根据二八原则,80% 的性能消耗其实是发生在 20% 的代码上,对热点代码的针对优化可以提升整体系统的性能。

然后我们在看看方法内联到底是什么?其实可以用以下例子粗鄙的解释:

private int add2(int x1 , int x2 , int x3 , int x4) {
return add1(x1 , x2) + add1(x3,x4);
}
  
private int add1(int x1 , int x2) {
return x1 + x2;
}

在运行一段时间后,代码可能就变成下面这样了:

private int add2(int x1 , int x2 , int x3 , int x4) {
//return add1(x1 , x2) + add1(x3,x4);
return x1 + x2 + x3 + x4;
}

嘿,看来方法内联就是把调用的方法函数代码复制到了调用函数内,减少因函数调用而带来的开销。

但是方法内联也是有条件的:

  1. 热点代码
  2. 方法体不能太大

方法内联会导致拷贝代码副本过多,代码占用内存增加,所以方法体不能太大。

其次由于方法可能被继承,导致需要类型检查而没有达到性能的效果,所以想要对热点代码进行方法内联,最好尽量使用 final、private、static 这些修饰方法,避免因为继承而导致额外的类型检查。

说到这,其实就能理解 final 能够提高效率的原因在于哪了,但是如果想通过 final 来提升性能,那几乎是不太可能。

现在,让我来回答一下 final 能够提高性能这个说法?

emmmm,这样的说法太片面。final 的确能够提升性能,但毕竟微乎其微。final 修饰的变量值多数情况下就能在编译阶段确定下来,这就可能导致某些代码被编译器优化掉。其次,关于方法内联的说法,也的确存在。但是方法内联也是需要条件的,首先是被频繁调用的热点代码,方法内联能够减少方法调用所带来的性能消耗;其次是方法体不能过大,原因很简单,方法内联将会导致拷贝代码更多,代码占用更多的内存空间。最后才是方法最好能被 final、private、static 修饰,这样就能避免过多的参数检查、类型检查等等。我们更应该关注 final 所带来的特性,比如属性不可变、方法不可重写、类不可继承,想要通过 final 来提升性能,几乎是不可能的事。

参考

深入理解Java中的final关键字

Final of Java,这一篇差不多了

final修饰递归方法会提高效率吗?

Java 方法内联