引言


大家好,我是你们的老伙计秀才!今日带来的是[浅显易懂Java多线程]系列的第八篇内容:volatile。大家觉得有用请点赞,喜爱请重视!秀才在此谢过大家了!!!

在当今的软件开发领域,多线程编程现已成为进步系统功能和响应速度的重要手段。Java作为广泛运用的多线程支撑语言,其内存模型(JMM)设计巧妙地处理了并发环境下同享资源访问时或许遇到的问题。但是,在多线程间同享数据时,程序员往往会遭受两个中心应战:内存可见性和指令重排序。

内存可见性问题首要体现在当一个线程修正了同享变量后,其他线程未必能当即感知到这个改变。在Java内存模型中,主内存与每个线程私有的作业内存相互独立,对变量的读写操作或许会先缓存在作业内存中,然后导致不同线程对同一变量值的认知出现误差。

指令重排序则是为了优化程序履行功率,编译器和CPU能够在不影响单线程语义的前提下重新安排指令履行次序。但是,在多线程环境下,这种优化或许导致意想不到的成果,破坏程序的正确性。

volatile要害字在Java多线程编程中起到了要害效果,它为处理上述问题供给了有用的东西。经过运用volatile润饰的变量,能够保证多个线程间的同享状态更新能够及时、准确地传播,而且制止编译器和处理器对其进行无序履行的优化。例如:

public class VolatileExample {
    volatile int sharedValue;
    public void writerThread() {
        sharedValue = 100; // 对volatile变量的写入操作将当即改写至主内存
    }
    public void readerThread() {
        int localValue = sharedValue; // 对volatile变量的读取操作会从主内存获取最新值
    }
}

在这个简略的示例中,sharedValue 被声明为volatile类型,保证了writer线程对sharedValue的修正能够被reader线程当即看到。接下来的内容将进一步讨论volatile是如何完结这些特性的,以及在实践运用中如何利用volatile来增强多线程代码的安全性和一致性。

基本概念回忆


在深入讨论Java多线程中volatile要害字的特性和运用之前,有必要首要回忆几个要害的概念。虽然之前的系列文章中现已讲过这些内容了,为了照料没看过之前系列文章的小伙伴,这儿快速带大家复习一下。假如对这部分内容感兴趣的小伙伴,能够去翻翻这个系列的其他文章。

内存可见性
内存可见性是Java并发编程中的一个中心议题。在Java内存模型(JMM)中,一切线程同享同一主内存区域,而每个线程有自己的作业内存(本地缓存)。当一个线程修正了主内存中的同享变量时,该改变或许并不会当即同步到其他线程的作业内存中,然后形成不同线程对同一变量值的读取不一致。例如:

public class VisibilityIssue {
    int sharedValue = 0;
    public void updateValue() {
        sharedValue = 1; // 线程A修正了sharedValue
    }
    public void readValue() {
        System.out.println(sharedValue); // 线程B或许无法当即看到线程A的更新
    }
}

运用volatile润饰符则能够保证内存可见性,使得线程A对sharedValue的修正能够马上对线程B可见。

重排序
为优化程序履行功能,编译器和处理器或许会改变代码指令的实践履行次序,这种现象称为重排序。它产生在多个层面,包含编译阶段的指令优化以及运行时CPU流水线上的动态调整。但是,在多线程环境下,无约束的重排序或许导致不可猜测的成果,破坏程序逻辑的一致性。

happens-before规矩
为了帮助程序员了解和操控多线程环境下的履行次序,JVM引进了happens-before规矩。这是一个隐含的保证,只要按照这些规矩编写代码,JVM就能保证指令在不同线程间按预期的次序履行。例如,程序中对某个变量的写操作先行产生于随后对该变量的读操作,则写入的数据必定对读取线程可见。

结合上述概念,volatile要害字在Java 5及今后版本中得到了增强,不仅保证了其润饰的变量具有内存可见性,还严厉约束了volatile变量与一般变量之间的重排序行为,然后保证了并发场景下数据的一致性和正确性。

volatile的内存语义


在Java多线程编程中,volatile要害字为变量供给了一种特殊的内存语义,保证了数据在多个线程间的正确同步和一致性。这部分将具体解释volatile如何保证内存可见性、制止重排序以及经过内存屏障完结这些特性的机制。

内存可见性保证
volatile润饰符保证了当一个线程修正volatile变量时,一切其他线程都能当即看到这个更新后的值。考虑以下示例:

public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    public void writer() {
        a = 1; // step 1
        flag = true; // step 2
    }
    public void reader() {
        if (flag) { // step 3
            System.out.println(a); // step 4
        }
    }
}

在这个比如中,假如flag没有被volatile润饰,那么线程A对a的修正或许不会及时反映到线程B读取的值上。但是,因为flag是volatile变量,在线程A写入后,JMM会强制将其值改写至主内存,而且在随后线程B读取flag时,会从主内存获取最新的值,并使得线程B本地缓存中的a失效,然后重新从主内存加载最新值。

制止重排序机制
旧版Java内存模型允许volatile变量与一般变量之间的重排序,这或许导致并发问题。为了纠正这一缺陷,JSR-133增强了volatile的内存语义,规则编译器和处理器不能随意重排volatile变量与其他变量的操作次序。

例如,在两层查看确定单例形式中,假如没有运用volatile润饰instance变量,则初始化进程或许会被重排序,导致回来未彻底初始化的目标实例。而volatile能够避免这种风险:

public class Singleton {
    private volatile static Singleton instance; // 运用volatile避免重排序
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 不会产生进程1-3-2的重排序
                }
            }
        }
        return instance;
    }
}

内存屏障效果
为了完结上述内存语义,JVM采用了内存屏障技能来约束编译器和处理器的重排序行为。内存屏障分为读屏障(Load Barrier)和写屏障(Store Barrier),它们别离起到阻挠屏障两边指令重排序和保证数据同步到主内存的效果。

具体来说,针对volatile变量的写操作,会在其前后刺进StoreStore和StoreLoad屏障;关于volatile变量的读操作,会在其前后刺进LoadLoad和LoadStore屏障。这些屏障的存在保证了volatile变量的写入对一切线程都可见,而且不会与其前后非volatile变量的读写操作产生重排序。

综上所述,volatile要害字经过内存可见性和制止重排序这两个要害特性,有用地保护了多线程环境下同享变量的一致性和正确性,成为Java并发编程中的重要东西。

volatile的内存屏障完结细节


Java虚拟机(JVM)为了保证volatile变量的内存可见性和制止重排序特性,采用了内存屏障这一底层硬件支撑机制。内存屏障在硬件层面上首要有两种类型:读屏障(Load Barrier)和写屏障(Store Barrier)。它们不仅能够阻挠屏障两边指令的重排序,还担任协调CPU缓存与主内存的数据同步。

当编译器生成字节码时,会在volatile变量相关的读写操作前后刺进特定类型的内存屏障:

  1. StoreStore屏障
    在每个volatile写操作前刺进StoreStore屏障,以保证在此屏障之前的一般写操作完结并改写至主内存之后,才会履行volatile变量的写入操作。例如:
int a = 1; // 一般写操作
volatile int v = 2; // volatile写操作
// 实践履行时,会在v的写操作前刺进StoreStore屏障,保证a的值已刷回主内存
  1. StoreLoad屏障
    在每个volatile写操作后刺进StoreLoad屏障,强制一切之前产生的写操作改写到主内存,而且使当前处理器中心上的本地缓存无效,这样后续任何线程对volatile或非volatile变量的读取都会从主内存获取最新的数据。
  2. LoadLoad屏障
    在每个volatile读操作后刺进LoadLoad屏障,用于保证在这次volatile读操作之后的其他读操作(不论是volatile还是非volatile)能读取到比它更早的读操作所看到的数据。
  3. LoadStore屏障
    在每个volatile读操作后再刺进LoadStore屏障,避免此volatile读取操作与其后的写操作之间产生重排序,保证在此屏障之后的一切写操作,必须在读取volatile变量的操作完结之后才干履行。

因为不同的处理器架构或许对内存屏障的支撑程度不同,Java内存模型采取了一种保存战略,在编译器等级统一刺进上述四种内存屏障,然后保证在任意平台上都能取得正确的volatile内存语义。

例如,在两层查看确定单例形式中,volatile要害字在instance变量声明处起着至关重要的效果。假如未运用volatile润饰,初始化进程或许会被重排序为如下错误序列:

Singleton instance; // 假定没有volatile润饰符
public static Singleton getInstance() {
    if (instance == null) { // 第一次查看
        synchronized (Singleton.class) {
            if (instance == null) { // 第2次查看
                instance = new Singleton(); // 分解为分配内存、初始化目标、设置引证三个进程
            }
        }
    }
    return instance;
}

若不运用volatile,初始化进程或许产生1-3-2的重排序,导致其他线程在实例初始化完结前就访问到了没有彻底初始化的目标。而volatile经过内存屏障的刺进,能够避免这种风险的重排序行为,保证了多线程环境下正确地创建单例目标。

volatile的实践运用和用途


作为轻量级同步机制
volatile在Java并发编程中扮演了轻量级的同步人物,它能够保证对单个变量的读/写操作具有原子性,而且供给了一种比锁更轻便的线程间通讯方式。例如,在以下场景中,咱们能够运用volatile来替代锁:

public class Counter {
    private volatile int count = 0;
    public void increment() {
        count++; // 单线程环境下,count++并不是原子操作,但在多线程环境下,
                 // volatile能保证每次自增后其他线程都能看到最新的值
    }
    public int getCount() {
        return count;
    }
}

尽管volatile供给了内存可见性和一定程度上的原子性,但它并不适合于需求保证复合操作全体原子性的场景,例如触及多个变量的操作或许复杂的临界区代码块。

制止重排序的运用场景
volatile的一个重要用途是制止编译器和处理器进行或许导致程序逻辑错误的重排序行为。特别是在多线程环境中,重排序或许破坏数据依赖关系,导致不可预期的成果。下面以“两层查看确定”单例形式为例说明这一点:

public class Singleton {
    private volatile static Singleton instance; // 运用volatile要害字
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) { // 第一次查看
            synchronized (Singleton.class) {
                if (instance == null) { // 第2次查看
                    instance = new Singleton(); // 实例化目标
                }
            }
        }
        return instance;
    }
}

在这个比如中,假如不运用volatile润饰instance变量,则实例化进程或许会被重排序为分配内存、设置引证但未初始化目标、然后回来引证的次序。而volatile能够经过刺进内存屏障避免这种错误的重排序,保证当getInstance()方法回来时,实例现已正确地初始化完结。

总之,volatile的要害效果在于它能在不引进复杂锁机制的前提下,完结对同享变量的简略同步与通讯。但是,开发者需求注意volatile不能替代锁用于处理复杂状态下的并发操控问题,而是应当依据具体运用场景选择最合适的同步东西。关于那些只需求坚持单一变量可见性及有序性的简略同步需求,volatile是一个高效且实用的选择。

总结


volatile要害字在Java多线程编程中扮演了至关重要的人物,它供给了内存可见性和制止重排序的保证,然后有用地提升了并发环境下的数据一致性与正确性。

首要,在内存可见性方面,volatile润饰的变量保证了当一个线程修正该变量时,其他线程能当即看到这个更新。例如,在如下代码示例中,当flag被设置为true时,一切读取它的线程都会感知到改变:

public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    public void writer() {
        a = 1;
        flag = true; // 线程A对flag的修正对其他线程当即可见
    }
    public void reader() {
        if (flag) { // 线程B能马上看到线程A设置的flag值
            System.out.println(a);
        }
    }
}

其次,volatile经过引进内存屏障机制严厉约束了编译器和处理器的重排序行为,避免因为优化而引发的数据不一致问题。特别是在单例形式中的“两层查看确定”场景,运用volatile要害字能够保证目标实例化进程不会因重排序导致回来未初始化的目标实例:

public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // volatile制止这儿的初始化进程重排序
                }
            }
        }
        return instance; // 回来已正确初始化的目标
    }
}

但是,尽管volatile供给了一种轻量级的同步机制,但其功能相对有限,仅适用于简略状态同享和单个变量的原子操作。关于触及复合操作或更复杂的临界区,锁仍然是完结更强同步操控的首选东西。因此,开发者需求依据实践需求权衡功能与安全性的考量,合理选择并运用volatile和锁来构建稳健、高效的并发程序。