Johuer's Blog

多学知识,精简代码

0%

Concurrency:volatile两种用法

1.volatile能够保证变量的可见性,但是并不能保证变量的原子性和有序性。

2.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

3.它会强制将对缓存的修改操作立即写入主存;

4.如果是写操作,它会导致其他CPU中对应的缓存行无效。

Java并发编程:volatile关键字解析

volatile关键字的两种用法

System.out.println引发的volatile和synchronize

Java中的双重检查锁(double checked locking)

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
public class Demo2 {

boolean shutdown;

public static void main(String[] args) {
Demo2 demo2 = new Demo2();

new Thread(() -> demo2.doWork()).start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> demo2.shutdown()).start();
}

public void shutdown() {
shutdown = true;
System.out.println("停止工作");
}

public void doWork() {
System.out.println("开始工作");
while (!shutdown) {
// do something...
}
System.out.println("已经停止工作");
}

}

运行结果如下:

1
2
开始工作
停止工作

并没有输出”已经停止工作”,说明线程doWork()并没有感觉到线程shutdown()状态的变化,从内存角度讲:

每个线程都有自己的工作内存,当线程dowork()启动时,会把主内存中的shudown=false拷贝一份到工作内存中,那么此线程一直使用的是副本shutdown=false,当线程shutdown()把变量shutdown更新为true同时更新到主内存时,线程dowork()shutdown一直还是使用自己工作内存中的副本false, 所以造成死循环。

增加关键字volatile

1
volatile boolean shutdown;

运行结果如下:

1
2
3
开始工作
停止工作
已经停止工作

关键点:

  1. 状态标志的状态转换是原子操作。例如上面的代码中,对布尔类型进行赋值操作,在Java中是原子性操作
  2. 只有一次性的状态转换。上面的代码中,状态标志位只是从false转换为true,并没有继续进行从true到false的转换等。这种转换的一次性杜绝了有序性问题的产生。

双重检查单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton{
private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}

instance = new DoubleCheckSingleton(); 这个操作不是原子性的;
这个操作可以划分为:

  1. 在Heap中开辟地址,进行对象初始化:new DoubleCheckSingleton()
  2. 将Heap中初始化完成的DoubleCheckSingleton对象地址,指向Thread Stack中的对象引用instance。

加入关键字volatile。使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。