0%

深入理解final关键字

final的基本语义

final在java中是非常常用的关键字,其语义和C++中的constant有类似之处,都代表不变的意思,其可以用在以下地方:

  1. 修饰变量,成员变量或者本地变量均可。
  2. 修饰方法的参数。
  3. 修饰方法。
  4. 修饰类。

final变量

final修饰的primitive常量有两种用法:

  1. compile-time就知道变量值的情况。
  2. run-time才知道变量值的情况。

比如:

1
2
3
4
public class FinalData {
private final int number1 = 9;
private final int number2 = rand.nextInt(10);
}

对compile-time就知道常量值的情况,编译器可以把一些计算放在compile-time,这样可以减少一些run-time的overhead。

final修饰的是引用的时候,只代表这个引用是常量,也就是被修饰的引用不能重新指向其他的变量,但是引用指向的实例是可以被更改的。Java并不提供关键字来修饰某个引用指向实例本身是不可更改的,但是用户可以自己定义immutable object,一个很好地例子就是java的string类。

无论final修饰的是成员变量还是本地变量,java都允许两种赋值方式:

  1. 声明的时候直接赋值。
  2. 声明的时候不赋值,之后在赋值,但是只能赋值一次。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FinalDataInitialization {
private final int number = 9;

public FinalDataInitialization() {
// Do something...
}
}

public class FinalDataInitialization {
private final int number;

public FinalDataInitialization() {
number = readDB();
// Do something...
}

public void methodA() {
number = 9; // Illegal!!
}
}

final参数

final修饰方法参数和修饰变量的语义差不多:

  1. final修饰的primitive变量不允许在方法当中更改。
  2. final修饰的引用不能更改指向,但引用指向的对象本身可以被更改。

final方法

final修饰方法一般有两个作用:

  1. 声明方法不能被override。
  2. 在早期的java,编译器可以把优化成inline call,这样可以减少调用方法的overhead。在现在的版本中已经不需要。

值得一提的是,所有private的方法都是implicitly final的,因为其不对子类可见,子类当然没法override,子类强行定义的话只会创建一个新的方法,而不是覆写父类的方法。

final修饰的方法依然可以被重载。

final类

final修饰的类无法被继承:

  1. 其成员变量可以自行决定是否要用final修饰。
  2. 所有方法都是implicitly final的,因为无法被继承,自然不会有子类去override。

final在多线程中的语义

final与有序性

JMM对final域的重排规则是:

  1. 禁止把对final域的写重排到当前对象构造函数之外。
  2. 如果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
30
31
32
33
34
35
36
37
public class Data {
public int num;
public final int finalNum;

public Data(int num, int finalNum) [
this.num = num;
this.finalNum = finalNum;
}
}

public class FinalDemo {
private static Data data;
private static final Data finalData;

public static void write() {
data = new Data(1, 2);
}

public static void read() {
if (data != null) {
int num = data.num;
int finalNum = data.finalNum;
}
}

public static void writeFinal() {
finalData = new Data(3, 4);
finalData.num = 5;
}

public static void readFinal() {
if (finalData != null) {
int num = finalData.num;
int finalNum = finalData.finalNum;
}
}
}

我们先看对data的操作,假设线程A执行write()的操作,随后线程B执行read():

  1. 由于规则1的存在,final域在data引用对线程B可见的时候可以保证已经赋值了。
  2. 所以只要线程B可以看见引用,那么可以保证final域的值对线程B可见。
  3. 对于普通域,对普通域的赋值可能重排到构造函数之外:比如可能先写入default value,之后再赋值。
  4. 所以对于普通域,read()的操作读取的结果并不一定是1。

对于finalData的操作,我们仍然假设线程A执行writeFinal(),随后线程B执行readFinal()

  1. 对于规则2的存在,final修饰的引用的成员域,在对引用可见的时候都已经赋值了。
  2. 所以只要线程B可以看见引用,finalData中的numfinalNum的值(3,4)都对线程B可见。
  3. 注意第28行对finalData.num赋值5的操作并无此可见性保证。需要保证这个成员域的可见,可以考虑使用volatile

final与可见性

一般而言,影响可见性的有两点:

  1. 内存和CPU缓存的不一致性。
  2. 底层编译器和处理器对指令的重排。

对于final修饰的primitive type,由于其不可修改性,所以不存在1的问题,那么final所禁止的重排可以保证引用可见时,其已经被赋值,所以可以保证可见性。

对于final修饰的引用:

  1. 同样由于final所禁止的重排规则,在引用可见的时候,所有成员变量的值已经可见,所以可以保证初始化之后的可见性。
  2. 但是由于其并不禁止对引用指向对象的修改,所以之后的修改操作并不保证可见性。

final与原子性

final没有任何原子性的保证。

Immutable Object与线程安全性

构造函数中的this指针逃逸

在讲Immutale Object之前我们先来谈谈this指针逃逸的问题。

即使final可以保证修饰域的可见性,但是如果this在构造函数完成之前就对其他线程可见(逃逸)的话,这种情况下仍然会出问题。如下例:

1
2
3
4
5
6
7
8
9
public class EscpaeDemo {
private final int num;
public EscapeDemo escapeDemo;

public EscapeDemo() {
num = 5;
escapeDemo = this;
}
}

由于第6行和第7行可能没有数据依赖,可能重排。那么假设先执行第7行,此时可能出现的情况是:

  1. escapeDemo引用,也就是this引用已经对其他线程可见,但final域还没有赋值。
  2. 另外一个线程读取escapeDemo引用,进而读取final域,然而此时final域还没有被赋值。

我们可以看到this指针逃逸带来的问题,就是在对象还没有被完全初始化之前,其就对其他线程可见了,这是一个十分容易导致错误的问题。当然上面的例子十分明显,还有一些不那么明显的例子:

1
2
3
4
5
6
7
8
9
10
public class ThisEscape {
public ThisEscape(EventSource source)
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}

这种情况乍看之下好像没有问题,但是EventListener实例其实包含了一个隐式的this指针,因为其调用了对象的doSomething()方法,这种情况下this指针也是逃逸。如果在对象初始化完成之前,doSomething()方法就被调用,就很有可能出问题。

上面的代码可以换成下面安全的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SafeListener {
private final EventListener listener;

private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}

public static SafeListner getInstance(EventSource souce) {
SafeListner safe = new SafeListener();
source.registerListener(safe.lisener);
return safe;
}
}

这样的话,保证了SafeListener在接受到任何的event之前就已经完成了初始化,从而解决了this逃逸的问题。

Immutable Objet

一个对象是immutable的,如果其满足下面三个条件:

  1. 其状态在初始化完成时候无法更改,换句话说,不会提供可能修改对象状态的方法。
  2. 所有成员域均用final修饰。
  3. 构造过程中this指针不会逃逸。

Immutable object永远都是线程安全的。

这个不难理解,因为当其初始化完成的时候,其内部的所有状态都对其他线程可见了;并且它一旦初始化完成,就不能更改,也就是只能被其他线程读,而不能写,自然就不存在race condition。

比如下面的例子,我们可以只用immutable object和volatile,就可以实现线程安全的class:

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
30
31
32
33
34
@Immutable
public class Cache {
private final BigInteger key;
private final BigInteger value;

public Cache(BigInteger key, BigInteger value) {
this.key = key;
this.value = value;
}

public BigInteger getValue(BigInteger key) {
if (key == null || !this.key.equals(key)) {
return null;
}
else {
return value;
}
}
}

@ThreadSafe
public class ExpensiveService {
private volatie Cache cache = new Cache(null, null);

public void processRequest(Request req, Response, resp) {
BigInteger key = keyFromReq(req);
BigInteger value = cache.getValue(key);
if (value == null) {
value = expensiveCalculation(key);
cache = new Cache(key, value);
}
encodeIntoResponse(resp, value);
}
}

以上代码是线程安全的:

  1. Cache class保证了原子性,因为无论何时keyvalue都是同步更新的。并且在初始化完成之前是对其他线程不可见的,所以其他线程读取的时候,一定keyvalue都是同步更新了之后的值。
  2. volatile保证了可见性,也就是cache被更新了之后,引用是立马对其他线程可见的。
  3. 假设有一个线程去更新cache,这个操作并不影响现在所有读取当前cache引用的线程,因为cache是immutable object,其他线程不能更改,只能重新new一个。

通过以上例子我们可以看出immutable object可以如何帮助我们在不用synchronized的情况下保证线程安全。

Safe Publication

我们定义publishing一个对象的意思为,让对象在当前的scope之外可见,比如:

  1. 赋值给其他的引用。
  2. 在方法中返回一个引用。
  3. 或者是将当前引用传递给其他的方法。

而在多线程环境中,对于mutable object,我们很容易不注意就会unsafely publish,比如:

1
2
3
4
5
6
7
public class UnsafePublish {
public Demo demo;

public UnsafePublish() {
demo = new Demo();
}
}

我们在这篇文章里讲过,new的操作不是原子性的,所以引用可见的时候,其还没初始化完全,所以就会出现unsafe publish的情况。

对于mutable object来讲,我们希望可以保证当其引用可见的时候,其内部状态也已经可见了,这样才能保证safe publication,一般来讲有下列方法:

  1. 在static initizlier里初始化要publish的对象。
  2. volatile修饰要publish的对象。
  3. AtomicReference存要publish的对象。
  4. final修饰其引用,并且保证其在初始化的时候不会发生this逃逸。
  5. 用锁来guard每次对publish的引用的读和写。

解释如下:

  1. 第一点是JVM的保证,static initializer的调用在构造函数之前,可以保证所有初始化的field在引用可见的时候已经可见了。
  2. 这点我们在上面的文章里讲过,volatile禁止重排序从而保证可见性。
  3. 3和5本质上是一样的,通过强制原子操作从而避免race condition。并且本身也有可见性的保证。
  4. 本文已经重点讲过,不再赘述。