Java并发 - volatile关键字

Java并发 - volatile关键字

1. volatile的作用详解

  • 可见性:
    • 当一个线程对 volatile 变量进行写操作时,这个变量的新值会立即被刷新到主内存,而不是在线程本地缓存中等待一段时间。
    • 当其他线程需要读取这个变量时,它会从主内存中重新加载,而不是使用线程本地缓存中的值。
  • 禁止指令重排序:
    • volatile 关键字禁止指令重排序,确保在执行到 volatile 变量的读操作之前,所有的写操作都已经完成。这可以防止出现一些意外的程序行为。
  • 不保证原子性:
    • volatile 保证可见性和禁止指令重排序,但并不保证对 volatile 变量的所有操作都是原子性的。如果一个变量的操作需要保证原子性,需要使用 synchronized 或其他锁机制。
  • 轻量级同步:
    • 相对于 synchronized 关键字,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
34
35
public class Singleton {
/**
** 正常实例化对象过程:
分配内存空间。
初始化对象。
将内存空间的地址赋值给对应的引用。

** 操作系统可以对对象实例化过程进行优化:
分配内存空间。
将内存空间的地址赋值给对应的引用。
初始化对象

多线程环境下就可能将一个未初始化的对象引用暴露
*/
// 可见性,在竞态条件的情况下,其中一个线程初始化instance的时候,instance会及时的刷新主内存
// 防止指令重排序 防止多线程环境下就可能将一个未初始化的对象引用暴露
private static volatile Singleton instance = null;

private Singleton() {
}

public static Singleton getSingleton() {
// 第一次检查,如果实例为空才进入同步块
if (null == instance) {
synchronized (Singleton.class) {
// 第二次检查,如果实例为空才进入同步块
// 在instance已初始化成功。已有线程已经阻塞在同步代码块外面,第一个线程释放锁,第二个线程立马获取锁成功
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}

2. volatile 的实现原理

  • 可见性保证:
    • 当一个线程写入一个 volatile 变量时,JVM 会将该变量的值立即刷新到主内存,而不是缓存在线程的本地工作内存中。
    • 当其他线程需要读取这个变量时,它会从主内存中重新加载,而不是使用线程本地工作内存中的值。
  • 禁止指令重排序:
    • volatile关键字通过插入内存屏障(memory barrier)来禁止指令重排序。内存屏障分为写屏障和读屏障:
      • 写屏障:在写入 volatile 变量之前,会插入写屏障,确保所有之前的操作都完成。
      • 读屏障:在读取 volatile 变量之后,会插入读屏障,确保所有后续的操作都在读取之后执行。
    • 写屏障和读屏障的作用是防止指令重排序,确保写入 volatile 变量的操作在其他操作之前完成,读取 volatile 变量的操作在其他操作之后进行。
  • 内存屏障的插入位置:
    • 对于写入操作,JVM 会在写入 volatile 变量之前插入写屏障。
    • 对于读取操作,JVM 会在读取 volatile 变量之后插入读屏障。

3. volatile 的应用场景

  1. 可见性需求:
    • 如果一个变量的值在一个线程中发生了改变,其他线程需要立即看到这个变化,那么可以考虑使用 volatile
  2. 无依赖性:
    • volatile 适用于对变量的写操作不依赖于当前值的场景。如果写操作依赖于当前值,考虑使用其他同步机制,如 synchronized
  3. 不涉及复合操作:
    • volatile 适用于单个变量的读写操作,但不适用于复合操作,即一系列操作的原子性需求。
  4. 不需要维持不变性条件:
    • 如果多个变量之间有复杂的不变性条件需要维护,考虑使用其他同步机制。

总的来说,volatile 主要用于简单的状态标记、标志位等情况,不适用于复杂的原子性操作。在需要保证一系列操作的原子性时,应该考虑使用更强大的同步机制,比如 synchronized

volatile 关键字通常用于确保在多线程环境中对共享变量的可见性和禁止指令重排序。

3.1 标志位或状态标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SharedResource {
private volatile boolean flag = false;

public void setFlag() {
// 一些操作
flag = true; // 将共享变量标记为true
}

public void doSomething() {
while (!flag) {
// 循环等待,直到共享变量flag变为true
}
// 执行操作,共享变量flag此时可见
}
}

volatile 可以确保对 flag 的修改对于其他线程是可见的,避免了循环等待线程陷入死循环的情况。

3.2 双重检查锁定(Double-Checked Locking):

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

private Singleton() {}

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

在多线程环境中,使用 volatile 修饰的 instance 变量确保了对单例对象的初始化在多线程环境中的正确性。

3.3 计数器和累加器:

1
2
3
4
5
6
7
8
9
10
11
public class Counter {
private volatile int count = 0;

public void increment() {
count++;
}

public int getCount() {
return count;
}
}

对计数器的读写操作都是原子的,但使用 volatile 可以确保对计数器的修改对于其他线程是可见的。

3.4 线程安全的单例模式:

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

private Singleton() {}

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

通过 volatile 保证了在多线程环境中对 instance 的读写操作的可见性。

3.5 避免死锁的双检锁(Double-Checked Locking):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Resource {
private static volatile Resource instance1 = new Resource();
private static volatile Resource instance2 = new Resource();

private Resource() {}

public static Resource getInstance1() {
return instance1;
}

public static Resource getInstance2() {
return instance2;
}
}

使用 volatile 避免了双检锁的死锁问题,确保在多线程环境中正确获取实例。


Java并发 - volatile关键字
http://example.com/2025/12/01/Java并发-volatile关键字/
作者
TuBoShu
发布于
2025年12月1日
许可协议