深入理解final关键字

final关键字特性

final关键字在java中使用非常广泛,可以申明成员变量、方法、类、本地变量。一旦将引用声明为final,将无法再改变这个引用。final关键字还能保证内存同步,本博客将会从final关键字的特性到从java内存层面保证同步讲解。这个内容在面试中也有可能会出现。

final使用

final变量

final变量有成员变量或者是本地变量(方法内的局部变量),在类成员中final经常和static一起使用,作为类常量使用。其中类常量必须在声明时初始化,final成员常量可以在构造函数初始化。

public class Main {
    public static final int i; //报错,必须初始化 因为常量在常量池中就存在了,调用时不需要类的初始化,所以必须在声明时初始化
    public static final int j;
    Main() {
        i = 2;
        j = 3;
    }
}

就如上所说的,对于类常量,JVM会缓存在常量池中,在读取该变量时不会加载这个类。


public class Main {
    public static final int i = 2;
    Main() {
        System.out.println("调用构造函数"); // 该方法不会调用
    }
    public static void main(String[] args) {
        System.out.println(Main.i);
    }
}

final方法

final方法表示该方法不能被子类的方法重写,将方法声明为final,在编译的时候就已经静态绑定了,不需要在运行时动态绑定。final方法调用时使用的是invokespecial指令。

class PersonalLoan{
    public final String getName(){
        return"personal loan”;
    }
}
 
class CheapPersonalLoan extends PersonalLoan{
    @Override
    public final String getName(){
        return"cheap personal loan";//编译错误,无法被重载
    }
    
    public String test() {
        return getName(); //可以调用,因为是public方法
    }
}

final类

final类不能被继承,final类中的方法默认也会是final类型的,java中的String类和Integer类都是final类型的。

final class PersonalLoan{}
 
class CheapPersonalLoan extends PersonalLoan {  //编译错误,无法被继承 
}

final关键字的知识点

  1. final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。final变量一旦被初始化后不能再次赋值。
  2. 本地变量必须在声明时赋值。 因为没有初始化的过程
  3. 在匿名类中所有变量都必须是final变量。
  4. final方法不能被重写, final类不能被继承
  5. 接口中声明的所有变量本身是final的。类似于匿名类
  6. final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
  7. final方法在编译阶段绑定,称为静态绑定(static binding)。
  8. 将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。

final方法的好处:

  1. 提高了性能,JVM在常量池中会缓存final变量
  2. final变量在多线程中并发安全,无需额外的同步开销
  3. final方法是静态编译的,提高了调用速度
  4. final类创建的对象是只可读的,在多线程可以安全共享

从java内存模型中理解final关键字

java内存模型对final域遵守如下两个重拍序规则

  1. 初次读一个包含final域的对象的引用和随后初次写这个final域,不能重拍序。
  2. 在构造函数内对final域写入,随后将构造函数的引用赋值给一个引用变量,操作不能重排序。

以上两个规则就限制了final域的初始化必须在构造函数内,不能重拍序到构造函数之外,普通变量可以。

具体的操作是

  1. java内存模型在final域写入和构造函数返回之前,插入一个StoreStore内存屏障,静止处理器将final域重拍序到构造函数之外。
  2. java内存模型在初次读final域的对象和读对象内final域之间插入一个LoadLoad内存屏障。

new一个对象至少有以下3个步骤

  1. 在堆中申请一块内存空间
  2. 对象进行初始化
  3. 将内存空间的引用赋值给一个引用变量,可以理解为调用invokespecial指令

普通成员变量在初始化时可以重排序为1-3-2,即被重拍序到构造函数之外去了。 final变量在初始化必须为1-2-3。

读写final域重拍序规则

public class FinalExample {
    int i;               
    final int j;
    static FinalExample obj;

    public void FinalExample () {
        i = 1;                   // 1
        j = 2;                   // 2
    }

    public static void writer () {  //写线程A  
        obj = new FinalExample ();  // 3
    }

    public static void reader () {       //读线程B执行
        if(obj != null) {               //4
            int a = object.i;           //5
            int b = object.j;           //6
        }
    }
}

我们可以用happens-before来分析可见性。结果是保证a读取到的值可能为0,或者1 而b读取的值一定为2。
首先,由final的重拍序规则决定3HB2,但是3和1不存在HB关系,原因在上面说过了。 因为线程B在线程A之后执行,所以3HB4。
那么2和4的HB关系怎么确定?? final的重拍序规则规定final的赋值必须在构造函数的return之前。所以2HB4。因为在一个线程内4HB6.所以可以得出结论2HB5。则b一定能得到j的最新值。而a就不一定了,因为没有HB关系,可以读到任意值。

HB判断可见性关系真是太方便了。可以参考我的另外一个博客http://medesqure.top/2018/08/25/happen-before/

可能发生的执行时序如下所示。


image

final对象是引用类型

如果final域是一个引用类型,比如引用的是一个int类型的数组。对于引用类型,写final域的重拍序规则增加了如下的约束

  1. 在构造函数内对一个final引用的对象的成员域的写入和随后在构造函数外将被构造对象的引用赋值给引用变量之间不能重拍序。 即先写int[]数组的内容,再将引用抛出去。
public class FinalReferenceExample {
    final int[] intArray;                     //final是引用类型
    static FinalReferenceExample obj;
    
    public FinalReferenceExample () {        //构造函数  在构造函数中不能被重排序 final类型在声明或者在构造函数中要赋值。
        intArray = new int[1];              //1
        intArray[0] = 1;                   //2
    }
    
    public static void writerOne () {          //写线程A执行
        obj = new FinalReferenceExample ();  //3
    }
    
    public static void writerTwo () {          //写线程B执行
        obj.intArray[0] = 2;                 //4
    }
    
    public static void reader () {              //读线程C执行
        if (obj != null) {                    //5
            int temp1 = obj.intArray[0];       //6
        }
    }
}

JMM保证了3和2之间的有序性。同样可以使用HB原则去分析,这里就不分析了。执行顺序如下所示。


6DBA7734-EFF8-4AC2-8E3B-E1645889A109

final引用不能从构造函数“逸出”

JMM对final域的重拍序规则保证了能安全读取final域时已经在构造函数中被正确的初始化了。
但是如果在构造函数内将被构造函数的引用为其他线程可见,那么久存在对象引用在构造函数中逸出,final的可见性就不能保证。 其实理解起来很简单,就是在其他线程的角度去观察另一个线程的指令其实是重拍序的。

public class FinalReferenceEscapeExample {
    final int i;
    static FinalReferenceEscapeExample obj;
    
    public FinalReferenceEscapeExample () {
        i = 1;       //1写final域
        obj = this;  //2 this引用在此“逸出”  因为obj不是final类型的,所以不用遵守可见性  }
    
    public static void writer() {
        new FinalReferenceEscapeExample ();
    }

    public static void reader {
        if (obj != null) {                     //3
            int temp = obj.i;                 //4
        }
    }
}

操作1的和操作2可能被重拍序。在其他线程观察时就会访问到未被初始化的变量i,可能的执行顺序如图所示。


AAF34760-7112-463C-852F-25CB775AFD62

本文结束,欢迎阅读。
本人博客 http://medesqure.top/ 欢迎观看

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容