volatile关键字

在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。

被volatile修饰的变量有如下特性:

  1. 使得变量更新变得具有可见性,只要被volatile修饰的变量的赋值一旦变化就会通知到其他线程,如果其他线程的工作内存中存在这个同一个变量拷贝副本,那么其他线程会放弃这个副本中变量的值,重新去主内存中获取
  2. 产生了内存屏障,防止指令进行了重排序,关于这点的解释,请看下面一段代码:
    1
    2
    3
    4
    5
    6
    7
    public class VolatileTest {
    int a = 0; //1
    int b = 1; //2
    volatile int c = 2; //3
    int d = 3; //4
    int e = 4; //5
    }
    在如上的代码中,因为c变量是用volatile进行修饰,那么就会对该段代码产生一个内存屏障,用以保证在执行语句3的时候语句1和语句2是绝对执行完毕的,而且在执行语句3的时候,语句4和语句5肯定没有执行。同时说明一下,在上述代码中虽然保证了语句3的执行顺序不可变换,但是语句1和语句2,语句4和语句5可能发生指令重排序哦。

总结: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 VolatileTest {
// int a = 0; //1
// int b = 1; //2
public static volatile int c = 0; //3
// int d = 3; //4
// int e = 4; //5
public static void increase(){
c++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
increase();
}
}).start();
}
Thread.sleep(5000);
System.out.println(c);
}
}
//运行3次结果分别是:997,995,989

多线程访问volatile关键字不会发生阻塞。
volatile关键字能保证数据的可见性,但不能保证数据的原子性。

volatile在双重检查锁实现单例中的作用

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

private volatile static Singleton uniqueInstance;

private Singleton() {
}

public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。


volatile关键字
https://www.wekri.com/java/volatile/
Author
Echo
Posted on
September 12, 2018
Licensed under