之前明明及得做过final关键字的学习笔记啊。。。嗯。。。在本地文档库里找了半天没找到,补上吧,复习一遍~
这一块主要是参考:「你听__」 你以为你真的了解final吗?
学习的,整理的非常好,推荐~
仅整理关键部分,部分详细见原博文(尤其第四部分、第六部分)
一、final简介
- final可以修饰变量,方法和类,用于表示所修饰的内容一旦赋值之后就不会再被改变,比如String类就是一个final类型的类。即使能够知道final具体的使用方法,我想对final在多线程中存在的重排序问题也很容易忽略,希望能够一起做下探讨。
二、final的具体使用场景
final能够修饰变量,方法和类,也就是final使用范围基本涵盖了java每个地方,下面就分别以锁修饰的位置:变量,方法和类分别来说一说。
2.1、变量
- 在java中变量,可以分为成员变量以及方法局部变量。因此也是按照这种方式依次来说,以避免漏掉任何一个死角。
2.1.1 final成员变量:类变量、实例变量
- 通常每个类中的成员变量可以分为类变量(static修饰的变量)以及实例变量。
- 针对这两种类型的变量赋初值的时机是不同的,类变量可以在声明变量的时候直接赋初值或者在静态代码块中给类变量赋初值;而实例变量可以在声明变量的时候给实例变量赋初值,在非静态初始化块中以及构造器中赋初值。
- 类变量有两个时机赋初值,而实例变量则可以有三个时机赋初值。当final变量未初始化时系统不会进行隐式初始化,会出现报错。
- 类变量:必须要在静态初始化块中指定初始值或者声明该类变量时指定初始值,而且只能在这两个地方之一进行指定;
- 实例变量:必须要在非静态初始化块,声明该实例变量或者在构造器中指定初始值,而且只能在这三个地方进行指定。
2.2.2、final局部变量
final局部变量由程序员进行显式初始化;
如果final局部变量已经进行了初始化则后面就不能再次进行更改;
如果final变量未进行初始化,可以进行赋值,当且仅有一次赋值,一旦赋值之后再次赋值就会出错。
final基本数据类型 VS final引用数据类型:
- 如果final修饰的是一个基本数据类型的数据,一旦赋值后就不能再次更改;
- 对于引用类型变量而言,它仅仅保存的是一个引用,final只保证这个引用类型变量所引用的地址不会发生改变,即一直引用这个对象,但这个对象属性是可以改变的。
宏变量:常量
利用final变量的不可更改性,在满足一下三个条件时,该变量就会成为一个“宏变量”,即是一个常量。
- 使用final修饰符修饰;
- 在定义该final变量时就指定了初始值;
- 该初始值在编译时就能够唯一指定。
注意:当程序中其他地方使用该宏变量的地方,编译器会直接替换成该变量的值。
2.2、方法
重写override:
- 当父类的方法被final修饰的时候,子类不能重写父类的该方法;
- 比如在Object中,getClass()方法就是final的,我们就不能重写该方法,但是hashCode()方法就不是被final所修饰的,我们就可以重写hashCode()方法。
重载overload:
被final修饰的方法是可以重载的。(稍微想想就是,肯定不受影响)
2.3、类
当一个类被final修饰时,表名该类是不能被子类继承的。
父类会被final修饰,当子类继承该父类的时候,就会报错。
由于子类继承往往可以重写父类的方法和改变父类属性,会带来一定的安全隐患,因此,当一个类不希望被继承时就可以使用final修饰。
三、final的例子
final经常会被用作不变类上,利用final的不可更改性。我们先来看看什么是不变类。
不变类的意思是:创建该类的实例后,该实例的实例变量是不可改变的。满足以下条件则可以成为不可变类:
- 使用private和final修饰符来修饰该类的成员变量
- 提供带参的构造器用于初始化类的成员变量;
- 仅为该类的成员变量提供getter方法,不提供setter方法,因为普通方法无法修改fina修饰的成员变量;
- 如果有必要就重写Object类 的hashCode()和equals()方法,应该保证用equals()判断相同的两个对象其Hashcode值也是相等的。
JDK中提供的八个包装类和String类都是不可变类,我们来看看String的实现:
/** The value is used for character storage. */
private final char value[];
可以看出String的value就是final修饰的,上述其他几条性质也是吻合的。
四、final在多线程中的应用
上面我们聊的final使用,应该属于Java基础层面的,当理解这些后我们就真的算是掌握了final吗?有考虑过final在多线程并发的情况吗?
在java内存模型中我们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。
同时,处理器和编译为了性能优化会对指令序列有编译器和处理器重排序。那么,在多线程情况下,final会进行怎样的重排序?会导致线程安全的问题吗?下面,就来看看final的重排序。
4.1、final域重排序规则:基本类型、引用类型
按照final修饰的数据类型分类:
基本数据类型:
- final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。
- final域读:禁止初次读对象的引用与读该对象包含的final域的重排序。
引用数据类型:
额外增加约束:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序。
五、final的实现原理
上面我们提到过,写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。
很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以StoreStore屏障可以省略。由于不会对有间接依赖性的操作重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说,以X86为例的话,对final域的读/写的内存屏障都会被省略!具体是否插入还是得看是什么处理器。