volatile关键字详解

简介

volatile 是Java中的一个关键字,它用于修饰变量,是Java提供的轻量级同步机制,在并发编程中扮演重要角色。相较于重量级锁(如synchronized),volatile更为轻量,避免了庞大的开销。

volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

20230921193230

适用场景:volatile 适用于对变量的读取和写入操作都是独立的、不依赖于其他变量的情况。常见的使用场景包括标志位的设置和检查、线程的启动和停止等。

特点

  • 可见性(Visibility):当一个变量被声明为 volatile 时,它保证了对该变量的读取和写入操作都将直接在主内存中进行,而不会使用线程的本地缓存(工作内存)。这意味着一个线程对 volatile 变量的修改对其他线程是可见的,即使其他线程使用了本地缓存,也能立即看到修改后的值。

  • 禁止指令重排序(Ordering):volatile 变量的读写操作具有禁止指令重排序的特性。这意味着在 volatile 变量的写操作之前的代码不会被重排序到写操作之后,保证了代码执行的有序性。这对于实现线程安全的双重检验锁方式等场景非常重要。

  • 不保证原子性(Atomicity):虽然 volatile 提供了可见性和有序性,但它不保证复合操作的原子性。如果一个操作包含多个步骤,每个步骤都需要保证原子性,那么仍然需要使用其他线程同步机制,如锁或原子类。

示例

双重校验锁实现对象单例(线程安全)

public class Singleton {
    // 使用 volatile 关键字确保多线程环境下的可见性
    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;
    }
}

双重检验锁方式实现单例模式的原理:

  • 我们将 instance 声明为 volatile 类型的变量。这是为了确保多线程环境下对 instance 的可见性,以防止出现线程安全问题。

  • 在 getInstance 方法中,首先检查 instance 是否为 null。如果 instance 不为 null,表示已经创建了单例对象,直接返回它,无需进一步的同步操作。

  • 如果 instance 为 null,进入同步块(synchronized block)。在同步块中,再次检查 instance 是否为 null,这是为了避免多个线程同时通过第一次检查,只有一个线程能够进入同步块。

  • 如果 instance 仍然为 null,则在同步块内创建一个新的 Singleton 实例,并将其赋值给 instance 变量。

这种双重检验锁方式实现单例模式的好处是,在大多数情况下,多个线程不需要进入同步块,因此提高了性能。只有在第一次创建单例对象时,才需要进行同步,保证了线程安全。这种方式被称为“双重检验锁”是因为它在两次检查 instance 变量是否为 null 后才创建实例。这是一种常见的用于实现懒汉式单例模式的方式。

volatile 如何禁止指令重排序?
instance = new Singleton(); 这段代码其实是分为三步执行:

  • 为 uniqueInstance 分配内存空间
  • 初始化 uniqueInstance
  • 将 uniqueInstance 指向分配的内存地址

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

volatile原子性测试


public class VolatileAtomicityDemo {
    public volatile static int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo();
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                for (int j = 0; j < 500; j++) {
                    volatileAtomicityDemo.increase();
                }
            });
        }
        // 等待1.5秒,保证上面程序执行完成
        Thread.sleep(1500);
        System.out.println(inc);
        threadPool.shutdown();
    }
}

正常情况下,运行上面的代码理应输出 2500。但你真正运行了上面的代码之后,你会发现每次输出结果都小于 2500。为什么会出现这种情况呢?不是说好了,volatile 可以保证变量的可见性嘛!
也就是说,如果 volatile 能保证 inc++ 操作的原子性的话。每个线程中对 inc 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是 5*500=2500。
很多人会误认为自增操作 inc++ 是原子性的,实际上,inc++ 其实是一个复合操作,包括三步:

  • 读取 inc 的值。
  • 对 inc 加 1。
  • 将 inc 的值写回内存。

volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:线程 1 对 inc 进行读取操作之后,还未对其进行修改。线程 2 又读取了 inc的值并对其进行修改(+1),再将inc 的值写回内存。线程 2 操作完毕后,线程 1 对 inc的值进行修改(+1),再将inc 的值写回内存。这也就导致两个线程分别对 inc 进行了一次自增操作后,inc 实际上只增加了 1。其实,如果想要保证上面的代码运行正确也非常简单,利用 synchronized、Lock或者AtomicInteger都可以。

使用 synchronized 改进:

public synchronized void increase() {
    inc++;
}

使用 AtomicInteger 改进:

public AtomicInteger inc = new AtomicInteger();
public void increase() {
    inc.getAndIncrement();
}

使用 ReentrantLock 改进:

Lock lock = new ReentrantLock();
public void increase() {
    lock.lock();
    try {
        inc++;
    } finally {
        lock.unlock();
    }
}

volatile可见性测试

public class VolatileExample {
    private volatile boolean flag = false;

    public void toggleFlag() {
        flag = !flag;
    }

    public boolean isFlag() {
        return flag;
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();

        Thread writerThread = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            example.toggleFlag();
            System.out.println("Flag is set to true.");
        });

        Thread readerThread = new Thread(() -> {
            while (!example.isFlag()) {
                // 等待flag变为true
            }
            System.out.println("Flag is true, exiting.");
        });

        writerThread.start();
        readerThread.start();
    }
}

在上述示例中,flag 变量被声明为 volatile,这确保了在 writerThread 修改 flag 时,readerThread 能够立即看到 flag 的修改,从而退出循环。这演示了 volatile 的可见性特性。

posted @ 2023-09-21 20:51  岸南  阅读(86)  评论(0)    收藏  举报