关键字final与volatile

本篇学习关键字final和volatile

final

在Java语言中,关键字final通常指“这是无法修改的”。之所以要采用final关键字,一般是出于性能和设计层面的考虑。

final数据

用final关键字修饰的属性,对于Java编译器来说就是一个“常量”。其特点是:

  1. 具体的值在编译期间就已经被确定

  2. 在运行时被初始化的值,不希望它被改变。

对于基本类型,其本身就存放于虚拟机栈内部,由于这些基本类型都是与底层数据类型直接对应的,一些确定的计算过程可以直接在编译期完成,优化了运行期的执行效率。

对于引用类型,我们已知引用本身其实也是存放于虚拟机栈中,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
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
import java.util.Scanner;

public class Demo implements Runnable{

private static volatile boolean flag = true;

@Override
public void run() {
while(flag){
}
System.out.println(Thread.currentThread().getName() + "执行完毕!");
}

public static void main(String[] args) {
Demo demo = new Demo();
new Thread(demo, "thread A").start();

System.out.println("main 线程正在运行!");
Scanner sc = new Scanner(System.in);
while(sc.hasNext()){
String value = sc.next();
if(value.equals("q")){
new Thread(()-> demo.stopThread()).start();
break;
}
}
System.out.println("主线程退出了");
}

private void stopThread() {
flag = false;
}
}

主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile 修饰,就有可能出现延迟。

这里我们需要注意,volatile并不能保证线程安全性!

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
public class VolatileDemo implements Runnable {
private static volatile int count = 0; //使用 volatile 修饰基本数据内存不能保证原子性

//private static AtomicInteger count = new AtomicInteger() ;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
count++;
//count.incrementAndGet() ;
}
}

public static void main(String[] args) {
VolatileDemo volatileDemo = new VolatileDemo();
Thread t1 = new Thread(volatileDemo, "t1");
Thread t2 = new Thread(volatileDemo, "t2");
t1.start();
//t1.join();
t2.start();
//t2.join();
for (int i = 0; i < 10000; i++) {
count++;
//count.incrementAndGet();
}
System.out.println("最终Count=" + count);
}
}

这里,三个线程(t1,t2,main)同时对一个int类型的count变量进行累加时,会发现最后的结果并不是30000

这是因为虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值,但 count ++ 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。

  • 所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。
  • 也可以使用 synchronize 或者是锁的方式来保证原子性。
  • 还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。

指令重排序

volatile 还可以防止 JVM 进行指令重排优化。

举一个伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static Map<String,String> map ;
private static volatile boolean flag = fasle ;

//以下方法发生在线程 A 中 初始化 Map
public void initMap(){
//耗时操作
map = getMap() ;//1
flag = true ;//2
}


//发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
sleep() ;
}
//dosomething
doSomeThing(value);
}

flag没有被volatile修饰时,JVM对1和2进行重排,导致map还没有被初始化,就有可能被线程B使用了。

加上volatile之后可以防止这样的重排优化,保证业务的正常性。

指令重排的应用

一个最经典的场景就是双重懒加载(DCL)的单例模式了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {

private static volatile Singleton singleton;

private Singleton() {
}

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//防止指令重排
singleton = new Singleton();
}
}
}
return singleton;
}
}

这里的 volatile 关键字主要是为了防止指令重排。

如果不用 ,singleton = new Singleton();,这段代码其实是分为三步:

  • 分配内存空间。(1)
  • 初始化对象。(2)
  • singleton 对象指向分配的内存地址。(3)

加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。

0%