本篇学习关键字final和volatile
final
在Java语言中,关键字final通常指“这是无法修改的”。之所以要采用final关键字,一般是出于性能和设计层面的考虑。
final数据
用final关键字修饰的属性,对于Java编译器来说就是一个“常量”。其特点是:
具体的值在编译期间就已经被确定
在运行时被初始化的值,不希望它被改变。
对于基本类型,其本身就存放于虚拟机栈内部,由于这些基本类型都是与底层数据类型直接对应的,一些确定的计算过程可以直接在编译期完成,优化了运行期的执行效率。
对于引用类型,我们已知引用本身其实也是存放于虚拟机栈中,final关键字只限制了对这个引用的更改,并不会限制对引用所指的实例化对象的变更。
空白(blank)final
一个final属性可以定义的时候不赋予初始的值,但是在其实际使用之前必定需要被初始化,通常final属性的初始化,只会位于构造函数中或者属性定义时的表达式。为此,一个类中final域就可以根据对象而有所不同,却又可以保持其恒定不变的特性。
final参数
Java允许在参数列表里以声明的方式将参数指明为final。这意味这你无法在方法中更改参数引用所指向的对象。
final方法
使用final方法的原因有两个。
第一个原因:可以把方法锁定,以防止任何继承类修改它的含义。
第二个原因:方法的调用过程采用内嵌机制,更为高效
在最近的Java版本里,虚拟机做了优化,因此不再需要使用final方法来进行优化了。应该让编译器和JVM去处理效率问题,只有在想要明确静止覆盖时,才将方法设置为final的。
类中所有private方法都隐式地指定为是final的。由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义。
这一问题会造成混淆。因为,如果你视图覆盖一个private方法(隐式是final的),似乎是奏效的,而且编译器也不会给出错误信息。
final类
当将某个类的整体定义为final时,就表明了对于该类的设计永不需要做任何变动,或者出于安全的考虑,它不能有子类。
由于子类禁止继承,所以final类中所有的方法都隐式指定为是final的,因为无法覆盖它们。在final类中可以给方法添加final修饰词,但这不会增添任何意义。
volatile
在Java语言中volatile关键字是一个类型修饰符。Java中volatile的作用:强制每次都直接读内存,阻止重排序,确保volatile类型的值一旦被写入缓存必定会被立即更新到主存。
内存可见性
由于 Java
内存模型(JMM
)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。
这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存。
如图所示:
在并发环境中,可能出现线程B读取到的数据是线程A更新之前的数据。
这时,就到volatile
出场了:
当一个变量被
volatile
修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。
注:volatile
修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。
内存可见性的应用
当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 volatile
来修饰:
1 | import java.util.Scanner; |
主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile
修饰,就有可能出现延迟。
这里我们需要注意,volatile并不能保证线程安全性!
1 | public class VolatileDemo implements Runnable { |
这里,三个线程(t1,t2,main)同时对一个int类型的count变量进行累加时,会发现最后的结果并不是30000
这是因为虽然
volatile
保证了内存可见性,每个线程拿到的值都是最新值,但count ++
这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。
- 所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。
- 也可以使用
synchronize
或者是锁的方式来保证原子性。 - 还可以用
Atomic
包中AtomicInteger
来替换int
,它利用了CAS
算法来保证了原子性。
指令重排序
volatile
还可以防止 JVM
进行指令重排优化。
举一个伪代码
1 | private static Map<String,String> map ; |
当flag
没有被volatile
修饰时,JVM
对1和2进行重排,导致map
还没有被初始化,就有可能被线程B使用了。
加上volatile
之后可以防止这样的重排优化,保证业务的正常性。
指令重排的应用
一个最经典的场景就是双重懒加载(DCL)的单例模式了:
1 | public class Singleton { |
这里的 volatile
关键字主要是为了防止指令重排。
如果不用 ,singleton = new Singleton();
,这段代码其实是分为三步:
- 分配内存空间。(1)
- 初始化对象。(2)
- 将
singleton
对象指向分配的内存地址。(3)
加上 volatile
是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。