final的基本语义
final
在java中是非常常用的关键字,其语义和C++中的constant
有类似之处,都代表不变的意思,其可以用在以下地方:
final变量
final
修饰的primitive常量有两种用法:
- compile-time就知道变量值的情况。
- run-time才知道变量值的情况。
比如:
1 | public class FinalData { |
对compile-time就知道常量值的情况,编译器可以把一些计算放在compile-time,这样可以减少一些run-time的overhead。
当final
修饰的是引用的时候,只代表这个引用是常量,也就是被修饰的引用不能重新指向其他的变量,但是引用指向的实例是可以被更改的。Java并不提供关键字来修饰某个引用指向实例本身是不可更改的,但是用户可以自己定义immutable object,一个很好地例子就是java的string类。
无论final
修饰的是成员变量还是本地变量,java都允许两种赋值方式:
- 声明的时候直接赋值。
- 声明的时候不赋值,之后在赋值,但是只能赋值一次。
例子如下:
1 | public class FinalDataInitialization { |
final参数
final
修饰方法参数和修饰变量的语义差不多:
final
修饰的primitive变量不允许在方法当中更改。final
修饰的引用不能更改指向,但引用指向的对象本身可以被更改。
final方法
final
修饰方法一般有两个作用:
- 声明方法不能被override。
- 在早期的java,编译器可以把优化成inline call,这样可以减少调用方法的overhead。在现在的版本中已经不需要。
值得一提的是,所有private
的方法都是implicitly final
的,因为其不对子类可见,子类当然没法override,子类强行定义的话只会创建一个新的方法,而不是覆写父类的方法。
被final
修饰的方法依然可以被重载。
final类
被final
修饰的类无法被继承:
- 其成员变量可以自行决定是否要用
final
修饰。 - 所有方法都是implicitly
final
的,因为无法被继承,自然不会有子类去override。
final在多线程中的语义
final与有序性
JMM对final
域的重排规则是:
- 禁止把对
final
域的写重排到当前对象构造函数之外。 - 如果
final
修饰的是引用,那么禁止对final
修饰的引用的成员变量的写入和构建的对象被赋值到引用上操作的重排序。
我们来看下面的例子:
1 | public class Data { |
我们先看对data
的操作,假设线程A执行write()
的操作,随后线程B执行read()
:
- 由于规则1的存在,
final
域在data
引用对线程B可见的时候可以保证已经赋值了。 - 所以只要线程B可以看见引用,那么可以保证
final
域的值对线程B可见。 - 对于普通域,对普通域的赋值可能重排到构造函数之外:比如可能先写入default value,之后再赋值。
- 所以对于普通域,
read()
的操作读取的结果并不一定是1。
对于finalData
的操作,我们仍然假设线程A执行writeFinal()
,随后线程B执行readFinal()
:
- 对于规则2的存在,
final
修饰的引用的成员域,在对引用可见的时候都已经赋值了。 - 所以只要线程B可以看见引用,
finalData
中的num
和finalNum
的值(3,4)都对线程B可见。 - 注意第28行对
finalData.num
赋值5的操作并无此可见性保证。需要保证这个成员域的可见,可以考虑使用volatile
。
final与可见性
一般而言,影响可见性的有两点:
- 内存和CPU缓存的不一致性。
- 底层编译器和处理器对指令的重排。
对于final
修饰的primitive type,由于其不可修改性,所以不存在1的问题,那么final
所禁止的重排可以保证引用可见时,其已经被赋值,所以可以保证可见性。
对于final
修饰的引用:
- 同样由于
final
所禁止的重排规则,在引用可见的时候,所有成员变量的值已经可见,所以可以保证初始化之后的可见性。 - 但是由于其并不禁止对引用指向对象的修改,所以之后的修改操作并不保证可见性。
final与原子性
final
没有任何原子性的保证。
Immutable Object与线程安全性
构造函数中的this指针逃逸
在讲Immutale Object之前我们先来谈谈this
指针逃逸的问题。
即使final
可以保证修饰域的可见性,但是如果this
在构造函数完成之前就对其他线程可见(逃逸)的话,这种情况下仍然会出问题。如下例:
1 | public class EscpaeDemo { |
由于第6行和第7行可能没有数据依赖,可能重排。那么假设先执行第7行,此时可能出现的情况是:
- escapeDemo引用,也就是this引用已经对其他线程可见,但
final
域还没有赋值。 - 另外一个线程读取escapeDemo引用,进而读取
final
域,然而此时final
域还没有被赋值。
我们可以看到this指针逃逸带来的问题,就是在对象还没有被完全初始化之前,其就对其他线程可见了,这是一个十分容易导致错误的问题。当然上面的例子十分明显,还有一些不那么明显的例子:
1 | public class ThisEscape { |
这种情况乍看之下好像没有问题,但是EventListener
实例其实包含了一个隐式的this
指针,因为其调用了对象的doSomething()
方法,这种情况下this
指针也是逃逸。如果在对象初始化完成之前,doSomething()
方法就被调用,就很有可能出问题。
上面的代码可以换成下面安全的写法:
1 | public class SafeListener { |
这样的话,保证了SafeListener
在接受到任何的event之前就已经完成了初始化,从而解决了this
逃逸的问题。
Immutable Objet
一个对象是immutable的,如果其满足下面三个条件:
- 其状态在初始化完成时候无法更改,换句话说,不会提供可能修改对象状态的方法。
- 所有成员域均用
final
修饰。 - 构造过程中
this
指针不会逃逸。
Immutable object永远都是线程安全的。
这个不难理解,因为当其初始化完成的时候,其内部的所有状态都对其他线程可见了;并且它一旦初始化完成,就不能更改,也就是只能被其他线程读,而不能写,自然就不存在race condition。
比如下面的例子,我们可以只用immutable object和volatile
,就可以实现线程安全的class:
1 |
|
以上代码是线程安全的:
Cache
class保证了原子性,因为无论何时key
和value
都是同步更新的。并且在初始化完成之前是对其他线程不可见的,所以其他线程读取的时候,一定key
和value
都是同步更新了之后的值。volatile
保证了可见性,也就是cache
被更新了之后,引用是立马对其他线程可见的。- 假设有一个线程去更新
cache
,这个操作并不影响现在所有读取当前cache
引用的线程,因为cache
是immutable object,其他线程不能更改,只能重新new
一个。
通过以上例子我们可以看出immutable object可以如何帮助我们在不用synchronized
的情况下保证线程安全。
Safe Publication
我们定义publishing一个对象的意思为,让对象在当前的scope之外可见,比如:
- 赋值给其他的引用。
- 在方法中返回一个引用。
- 或者是将当前引用传递给其他的方法。
而在多线程环境中,对于mutable object,我们很容易不注意就会unsafely publish,比如:
1 | public class UnsafePublish { |
我们在这篇文章里讲过,new
的操作不是原子性的,所以引用可见的时候,其还没初始化完全,所以就会出现unsafe publish的情况。
对于mutable object来讲,我们希望可以保证当其引用可见的时候,其内部状态也已经可见了,这样才能保证safe publication,一般来讲有下列方法:
- 在static initizlier里初始化要publish的对象。
- 用
volatile
修饰要publish的对象。 - 用
AtomicReference
存要publish的对象。 - 用
final
修饰其引用,并且保证其在初始化的时候不会发生this
逃逸。 - 用锁来guard每次对publish的引用的读和写。
解释如下:
- 第一点是JVM的保证,static initializer的调用在构造函数之前,可以保证所有初始化的field在引用可见的时候已经可见了。
- 这点我们在上面的文章里讲过,
volatile
禁止重排序从而保证可见性。 - 3和5本质上是一样的,通过强制原子操作从而避免race condition。并且本身也有可见性的保证。
- 本文已经重点讲过,不再赘述。