Java 中的 volatile 关键字及其应用
在并发编程中,volatile 关键字是一个非常重要的概念。它主要用于保证变量的可见性和禁止指令重排序,但并不保证原子性。本文将详细介绍 volatile 关键字的作用、适用场景以及常见的坑。
1 volatile 的作用
-
保证可见性:
- 当一个线程写入一个
volatile变量时,JMM 会把该线程在本地内存中的变量强制刷新到主内存中去。 - 这个写操作会导致其他线程中的
volatile变量缓存无效。
- 当一个线程写入一个
-
禁止指令重排序:
volatile关键字可以禁止指令重排序,确保变量的读写操作按照程序的顺序执行。
2 指令重排序与 volatile
在 Java 内存模型(JMM)中,指令重排序是为了优化性能,但可能会导致多线程环境下的问题。volatile 关键字通过插入内存屏障来禁止指令重排序。
- 写屏障(Write Barrier):当一个
volatile变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。 - 读屏障(Read Barrier):当读取一个
volatile变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。
也就是说,执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见。
3 volatile 的适用场景
volatile 适用于以下场景:
- 状态标记:用于标记某个状态,例如线程是否终止。
- 双重检查锁定(Double-Checked Locking):用于实现线程安全的单例模式。
4 volatile 不适用的场景
volatile 不保证原子性,因此不适用于需要原子操作的场景。例如,自增操作 inc++ 不是一个原子操作,它由读取、加法和赋值三个步骤组成。
public class VolatileTest {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println("inc output:" + test.inc);
}
}
输出结果可能小于 10000,因为 inc++ 不是原子操作。
5 解决方法
- 使用 synchronized:
public class VolatileTest1 {
public int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final VolatileTest1 test = new VolatileTest1();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println("add synchronized, inc output:" + test.inc);
}
}
- 使用 Lock:
public class VolatileTest2 {
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
inc++;
lock.unlock();
}
public static void main(String[] args) {
final VolatileTest2 test = new VolatileTest2();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println("add lock, inc output:" + test.inc);
}
}
- 使用 AtomicInteger:
public class VolatileTest3 {
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
public static void main(String[] args) {
final VolatileTest3 test = new VolatileTest3();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println("add AtomicInteger, inc output:" + test.inc);
}
}
6 volatile实现单例模式的双重锁
在单例模式中,双重检查锁定(Double-Checked Locking)是一种常见的实现方式。为了防止指令重排序导致的问题,需要使用 volatile 关键字。
public class Penguin {
private static volatile Penguin m_penguin = null;
private int money = 10000;
private Penguin() {}
public void beating() {
System.out.println("打豆豆" + money);
}
public static Penguin getInstance() {
if (m_penguin == null) {
synchronized (Penguin.class) {
if (m_penguin == null) {
m_penguin = new Penguin();
}
}
}
return m_penguin;
}
}
在这个例子中,volatile 关键字确保了 m_penguin 变量的可见性和禁止指令重排序,从而避免了多线程环境下可能出现的“半初始化”问题。
7 小结
volatile 关键字在 Java 并发编程中扮演着重要的角色,它保证了变量的可见性和禁止指令重排序,但不保证原子性。在实际应用中,volatile 适用于状态标记和双重检查锁定等场景。对于需要原子操作的场景,可以使用 synchronized、Lock 或 AtomicInteger 等机制来保证线程安全。
通过理解 volatile 的工作原理和适用场景,开发者可以更好地编写线程安全的代码,避免并发编程中的常见问题。
8 思维导图

9 参考链接
更多推荐


所有评论(0)