final也是并发中一个常用的tips。JMM中对final提供的重排序规则如下:
1、当在constructor中对final变量写入之前,不会将构造对象的引用给出去。
2、读一个包含final域的对象的引用 happens before 后续对这个final域的读
关于(1),是说当在constructor内包含对final的写时,虽然constructor不是原子的,但在JMM会在对final的写之后,在constructor return之前插入一条内存屏障,这样就保证了在constructor将对象给到另一个线程的时候,是一定能看见对final域的写的结果的。看代码:
public class FinalExample { int i; final int j; static FinalExample obj; // constructor public void FinalExample () { i = 1; j = 2; } public static void writer () { obj = new FinalExample(); } public static void reader () { FinalExample object = obj; int a = object.i; int b = object.j; } }
现假设一个线程A执行writer方法,另一个线程B随后开始执行read方法。假设变量j不是final的,那么A在调用write时,constructor会发生两次写操作,这个时候线程B来调用reader读字段的时候,由于constructor方法不是原子的,又没有任何同步手段来协调线程A与B的同步,这时候就会乱掉了,比如constructor两次赋值发生了重排序,或者只完成了一次赋值,线程B就拿到构造一半的对象进行读值了,等等并发问题了。
但是,当把j变量定义为final时,就会发生一点点的变化了。
(1)final能够保证,在对象引用被任意线程可见之前,对象的final域一定被正确初始化了。而实现这种保证的手段,就是通过内存屏障,保证final域的赋值不会被重排序到constructor return之后。
(2)final能够保证,初次读对象引用于初次读该对象所包含的final域,不会发生重排序。所以在执行read方法时,final能够保证一定是先确实拿到了obj的引用之后,才能读obj中的final域j。
而实际上,IDEA对于final的赋值只能在final类变量定义时,不能将final的赋值推迟到constructor里。