大家好,我是大明哥,一个专心「死磕 Java」系列创造的硬核程序员。 本文已收录到我的技能网站:skjava.com。有全网最优质的系列文章、Java 全栈技能文档以及大厂完整面经。


答复

volatile 是一种轻量级的同步机制,它能确保同享变量的可见性,一起禁止重排序确保了操作的有序性,可是它无法确保原子性。所以运用 volatile 必须要满意这两个条件:

  1. 写入变量不依靠当时值。
  2. 变量不参加与其他变量的不变性条件。

volatile 比较合适多个线程读,一个线程写的场合,典型的场景有如下几个:

  1. 状况标志
  2. 重查看确定的单例形式
  3. 开支较低的“读-写锁”策略

详解

volatile 运用条件

要想正确安全地运用 volatile ,必须要具备这两个条件:

  • 写入变量不依靠当时值:变量的新值不能依靠于之前的旧值。假如变量的当时值与新值之间存在依靠联系,那么仅运用 volatile 是不够的,由于它不能确保一系列操作的原子性。比方 i++。
  • 变量不参加与其他变量的不变性条件:假如一个变量是与其他变量共同参加不变性条件的一部分,那么简单地声明变量为 volatile 是不够的。

第一个条件很好理解,第二个条件这儿需求解释下。

“变量不参加与其他变量的不变性条件”,这儿的“不变性条件”指的是一个或多个变量在程序履行过程中需求保持的条件或联系,以确保程序的正确性。假定咱们有两个变量,它们需求满意某种联系(例如,a + b = 99)。咱们需求在多线程环境下确保这种关闭在任何时候都是建立的。假如这个时候咱们只是将其中一个变量声明为 volatile,尽管确保了这个变量的更新对其他线程当即可见,但却不能确保这两个变量作为一个全体满意特定的不变性条件。在更新这两个变量的过程中,其他线程可能会看到这些变量处于不一致的状况。在这种情况下咱们就需求运用锁或许其他同步机制来确保这种联系的全体一致性。

volatile 运用场景

volatile 比较合适多个线程读,一个线程写的场合

状况标志

当咱们需求用一个变量来作为状况标志,操控线程的履行流程时,运用 volatile 能够确保当一个线程修改了这个标志时,其他线程能够当即看到最新的值。

public class TaskRunner implements Runnable {
    private volatile boolean running = true;  // 状况标志,操控任务是否持续履行
    public void run() {
        while (running) {  // 查看状况标志
            // 履行任务
            doSomething();
        }
    }
    public void stop() {
        running = false;  // 修改状况标志,使得线程能够停止履行
    }
    private void doSomething() {
        // 实践任务逻辑
    }
}

DCL 的单例形式

在完成单例形式时,为了确保线程安全,一般运用双重查看确定(Double-Checked Locking)形式。在这种形式中,volatile 用于避免单例实例的初始化过程中的指令重排序,确保其他线程看到一个彻底初始化的单例目标,具体来说,就是运用 volatile防止了Java 目标在实例化过程中的指令重排,确保在目标的结构函数履行结束之前,不会将 instance 的内存分配操作指令重排到结构函数之外。

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;
    }
}

开支较低的“读-写锁”策略

这种策略一般都是答应多个线程一起读取一个资源,但只答应一个线程写入的同步机制。这种“读-写锁”十分合适读多写少的场景,咱们能够运用 volatile + 锁的机制削减公共代码途径的开支。如下:

public class VolatileTest {
    private volatile int value;  
    //读,不加锁,提供功率 
    public int getValue() {   
        return value;   
    }   
    //写操作,运用锁,确保线程安全  
    public synchronized int increment() {  
        return value++;  
    }  
}

在 J.U.C 中,有一个采用“读-写锁”方式的类:ReentrantReadWriteLock,它包括两个锁:一个是读锁,另一个是写锁。

下面是伪代码:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class DataStructure {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Object data = ...; // 被保护的数据
    public void read() {
        readWriteLock.readLock().lock(); // 获取读锁
        try {
            // 履行读操作
            // 例如,读取data的内容
        } finally {
            readWriteLock.readLock().unlock(); // 开释读锁
        }
    }
    public void write(Object newData) {
        readWriteLock.writeLock().lock(); // 获取写锁
        try {
            // 履行写操作
            // 例如,修改data的内容
        } finally {
            readWriteLock.writeLock().unlock(); // 开释写锁
        }
    }
}
  • 读操作 :多个线程能够一起持有读锁,因而多个线程能够一起履行 read() 办法。
  • 写操作: 只有一个线程能够持有写锁,并且在持有写锁时,其他线程不能读取或写入。

这种“读-写锁”策略提高了在多线程环境下对同享资源的读取功率,尤其是在读操作远远多于写操作的情况下。可是,它也会让咱们的程序改变愈加杂乱,比方潜在的读写锁冲突、锁升级(从读锁升级到写锁)等问题。因而,在实践运用中,大明哥推荐直接运用 ReentrantReadWriteLock 即可,无需头铁自己造轮子。