在并发编程中,volatile 关键字是一个非常重要的概念。它主要用于保证变量的可见性和禁止指令重排序,但并不保证原子性。本文将详细介绍 volatile 关键字的作用、适用场景以及常见的坑。

1 volatile 的作用

  1. 保证可见性

    • 当一个线程写入一个 volatile 变量时,JMM 会把该线程在本地内存中的变量强制刷新到主内存中去。
    • 这个写操作会导致其他线程中的 volatile 变量缓存无效。
  2. 禁止指令重排序

    • volatile 关键字可以禁止指令重排序,确保变量的读写操作按照程序的顺序执行。

2 指令重排序与 volatile

在 Java 内存模型(JMM)中,指令重排序是为了优化性能,但可能会导致多线程环境下的问题。volatile 关键字通过插入内存屏障来禁止指令重排序。

  • 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。
  • 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。

也就是说,执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见。

3 volatile 的适用场景

volatile 适用于以下场景:

  1. 状态标记:用于标记某个状态,例如线程是否终止。
  2. 双重检查锁定(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 解决方法

  1. 使用 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);
    }
}
  1. 使用 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);
    }
}
  1. 使用 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 适用于状态标记和双重检查锁定等场景。对于需要原子操作的场景,可以使用 synchronizedLockAtomicInteger 等机制来保证线程安全。

通过理解 volatile 的工作原理和适用场景,开发者可以更好地编写线程安全的代码,避免并发编程中的常见问题。

8 思维导图

在这里插入图片描述

9 参考链接

Java volatile关键字解析

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐