java volatile 关键字理解

Java语言包括两种同步机制:同步块和volatile变量,都是为了实现线程的安全性。其中volatile同步性相对弱一些,所以相对容易出错;

volatile变量

volitile关键字用来修饰字段,告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性;
通过同步锁实现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
class VolatileFeaturesExample {
volatile long vl = 0L;
public void set(long vl) {
this.vl = vl;
}
public void getAndIncrement() {
vl ++;
}
public long get() {
return vl;
}
}
class VolatileFeaturesExample {
long vl = 0L;
public synchronized void set(long vl) {
this.vl = vl;
}
public void getAndIncrement() {
long temp = get();
temp += 1L;
set(temp);
}
public synchronized long get() {
return vl;
}
}

正确使用volatile变量的条件

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中

综上所述:被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

使用volatile关键字的场景

状态标记量

1
2
3
4
5
6
7
8
9
volatile boolean flag;
public void setFlag(boolean flag) {
this.flag = flag;
}
public void doWork() {
while (!flag) {
// do stuff
}
}

一次性安全发布

在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。
实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型。示例清单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singleton {
private volatile static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance)
instance = new Singleton();
}
}
return instance;
}
}

这里为什么需要instance变量使用volatile关键字修饰,因为变量的创建分为两步:

  • 1、分配对象的内存空间
  • 2、初始化对象
  • 3、设置变量指向内存地址
    然而在一些JIT编译器上,上述流程中的2、3步可能会被重排序。
    根据Java语言规范,所有线程在执行Java程序时必须要遵守intra-thread semantics。
    intra-thread semantics 保证重排序不会改变单线程内的程序执行过程。换句话说,intra-thread semantics 允许哪些在单线程内,不会改变单线程程序执行结果的重排序。故上述流程的2和3之间虽然重排序了,但这个重排序不会违反intra-thread semantics 。这个重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能。

开销较低的读-写锁策略

volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。
然而,如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。代码清单如下:

1
2
3
4
5
6
7
8
9
class CheeysCounter {
private volatile int value;
public int getValue() {
return value;
}
public synchronized int increment() {
return value ++;
}
}

因为本例中的写操作违反了使用 volatile 的第一个条件,因此不能使用 volatile 安全地实现计数器,所以必须使用锁。
故可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有变化的操作,使用 volatile 进行只读操作。