关于java:8000字就说一个字Volatile

37次阅读

共计 12342 个字符,预计需要花费 31 分钟才能阅读完成。

简介

volatile 是 Java 提供的一种轻量级的同步机制。Java 语言蕴含两种外在的同步机制:同步块(或办法)和 volatile 变量,相比于 synchronized(synchronized 通常称为重量级锁),volatile 更轻量级,因为它不会引起线程上下文的切换和调度。然而 volatile 变量的同步性较差(有时它更简略并且开销更低),而且其应用也更容易出错。

Java volatile关键字用于将 Java 变量标记为“存储在主存储器中”。更确切地说,这意味着,每次读取一个 volatile 变量都将从计算机的主内存中读取,而不是从 CPU 缓存中读取,并且每次写入 volatile 变量都将写入主内存,而不仅仅是 CPU 缓存。

实际上,自 Java 5 以来,volatile关键字保障的不仅仅是向主存储器写入和读取 volatile 变量。我将在以下局部解释。

个性

能够把对 volatile 变量的单个读 / 写,看成是应用同一个锁对这些单个读 / 写操作做了同步

当咱们申明共享变量为 volatile 后,对这个变量的读 / 写将会很特地。了解 volatile 个性的一个好办法是:把对 volatile 变量的单个读 / 写,看成是应用同一个锁对这些单个读 / 写操作做了同步。

COPYclass VolatileFeaturesExample {
    // 应用 volatile 申明 64 位的 long 型变量
    volatile long vl = 0L;

    public void set(long l) {vl = l;   // 单个 volatile 变量的写}

    public void getAndIncrement () {vl++;    // 复合(多个)volatile 变量的读 / 写}

    public long get() {return vl;   // 单个 volatile 变量的读}
}

假如有多个线程别离调用下面程序的三个办法,这个程序在语义上和上面程序等价:

COPYclass VolatileFeaturesExample {
    long vl = 0L;               // 64 位的 long 型一般变量

    // 对单个的一般 变量的写用同一个锁同步
    public synchronized void set(long l) {vl = l;}

    public void getAndIncrement () { // 一般办法调用
        long temp = get();           // 调用已同步的读办法
        temp += 1L;                  // 一般写操作
        set(temp);                   // 调用已同步的写办法
    }
    public synchronized long get() { 
        // 对单个的一般变量的读用同一个锁同步
        return vl;
    }
}

如下面示例程序所示,对一个 volatile 变量的单个读 / 写操作,与对一个一般变量的读 / 写操作应用同一个锁来同步,它们之间的执行成果雷同。

锁的 happens-before 规定保障开释锁和获取锁的两个线程之间的内存可见性,这意味着对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最初的写入。

锁的语义决定了临界区代码的执行具备原子性。这意味着即便是 64 位的 long 型和 double 型变量,只有它是 volatile 变量,对该变量的读写就将具备原子性。如果是多个 volatile 操作或相似于 volatile++ 这种复合操作,这些操作整体上不具备原子性。

简而言之,volatile 变量本身具备下列个性:

原子性

即一个操作或者多个操作 要么全副执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性是回绝多线程操作的,不论是多核还是单核,具备原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a= 1 是原子性操作,然而 a ++ 和 a += 1 就不是原子性操作。Java 中的原子性操作包含:

  • 根本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的互相赋值不是原子性操作。
  • 所有援用 reference 的赋值操作
  • java.concurrent.Atomic.* 包中所有类的所有操作

可见性

指当多个线程拜访同一个变量时,一个线程批改了这个变量的值,其余线程可能立刻看失去批改的值。

在多线程环境下,一个线程对共享变量的操作对其余线程是不可见的。Java 提供了 volatile 来保障可见性,当一个变量被 volatile 润饰后,示意着线程本地内存有效,当一个线程批改共享变量后他会立刻被更新到主内存中,其余线程读取共享变量时,会间接从主内存中读取。当然,synchronize 和 Lock 都能够保障可见性。synchronized 和 Lock 能保障同一时刻只有一个线程获取锁而后执行同步代码,并且在开释锁之前会将对变量的批改刷新到主存当中。因而能够保障可见性。

在线程应用非 volatile 变量的多线程应用程序中,出于性能起因,每个线程能够在解决它们时将变量从主存储器拷贝到 CPU 高速缓存中。如果您的计算机蕴含多个 CPU,则每个线程能够在不同的 CPU 上运行。这意味着,每个线程都能够将变量复制到不同 CPU 的 CPU 缓存中。这在这里阐明:

对于 volatile 变量,无奈保障 Java 虚拟机(JVM)何时将数据从主内存读取到 CPU 缓存中,或将数据从 CPU 缓存写入主内存。这可能会导致一些问题,我将在以下局部中解释。

设想一下两个或多个线程能够访问共享对象的状况,该共享对象蕴含一个申明如下的计数器变量:

COPYpublic class SharedObject {public int counter = 0;}

再设想一下,只有线程 1 对 counter 变量进行减少操作,但线程 1 和线程 2 都可能读取变量counter

如果 counter 变量未声明 volatile,则无奈保障何时将counter 变量的值从 CPU 缓存写回主存储器。这意味着,CPU 高速缓存中的 counter 变量值可能与主存储器中的变量值不同。这种状况如下所示:

线程没有看到变量的最新值的问题,是因为它还没有被另一个线程写回主内存,这被称为“可见性”问题,其余线程看不到一个线程的某些更新。

volatile 可见性保障

Java volatile关键字旨在解决变量可见性问题。通过应用 volatile 申明 counter 变量,对变量 counter 的所有写操作都将立刻写回主存储器。此外,counter变量的所有读取都将间接从主存储器中读取。

上面是 counter 变量申明为 volatile 的样子:

COPYpublic class SharedObject {public volatile int counter = 0;}

申明变量为 volatile,对其余线程写入该变量 保障了可见性

在下面给出的场景中,一个线程(T1)批改计数器,另一个线程(T2)读取计数器(但从不批改它),申明该 counter 变量为 volatile 足以保障写入 counter 变量对 T2 的可见性。

然而,如果 T1 和 T2 都在减少 counter 变量,那么申明 counter 变量为 volatile 就不够了。稍后会具体介绍。

齐全 volatile 可见性保障

实际上,Java volatile的可见性保障超出了 volatile 变量自身。可见性保障如下:

  • 如果线程 A 写入 volatile 变量并且线程 B 随后读取这个 volatile 变量,则在写入 volatile 变量之前对线程 A 可见的所有变量在线程 B 读取 volatile 变量后也将对线程 B 可见。
  • 如果线程 A 读取 volatile 变量,则读取 volatile 变量时对线程 A 可见的所有变量也将从主存储器从新读取。

让我用代码示例阐明:

COPYpublic class MyClass {
    private int years;
    private int months
    private volatile int days;

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

udpate()办法写入三个变量,其中只有 days 是 volatile 变量。

齐全 volatile 可见性保障意味着,当将一个值写入 days 时,对线程可见的其余所有变量也会写入主存储器。这意味着,当一个值被写入 daysyearsmonths的值也被写入主存储器(留神 days 的写入在最初)。

当读取 yearsmonthsdays的值你能够这样做:

COPYpublic class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

留神 totalDays() 办法通过读取 days 的值到 total 变量中开始。当读取 days 的值时,后续 monthsyears 值的读取也会从主存储器中读取。因而应用上述读取序列能够保障看到最新的 daysmonthsyears值。

有序性

即程序执行的程序依照代码的先后顺序执行。

java 内存模型中的有序性能够总结为:如果在本线程内察看,所有操作都是有序的;如果在一个线程中察看另一个线程,所有操作都是无序的。前半句是指“线程内体现为串行语义”,后半句是指“指令重排序”景象和“工作内存主主内存同步提早”景象。
​ 在 Java 内存模型中,为了效率是容许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行后果,然而对多线程会有影响。Java 提供 volatile 来保障肯定的有序性。最驰名的例子就是单例模式外面的 DCL(双重查看锁)。另外,能够通过 synchronized 和 Lock 来保障有序性,synchronized 和 Lock 保障每个时刻是有一个线程执行同步代码,相当于是让线程程序执行同步代码,天然就保障了有序性。

volatile 变量的个性

保障可见性,不保障原子性

  • 当写一个 volatile 变量时,JMM 会把该线程本地内存中的变量强制刷新到主内存中去;
  • 这个写会操作会导致其余线程中的缓存有效。

禁止指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种伎俩。重排序须要恪守肯定规定:

  • 重排序操作不会对存在数据依赖关系的操作进行重排序。

    比方:a=1;b=a; 这个指令序列,因为第二个操作依赖于第一个操作,所以在编译时和处理器运

    行时这两个操作不会被重排序。

  • 重排序是为了优化性能,然而不管怎么重排序,单线程下程序的执行后果不能被扭转

    比方:a=1;b=2;c=a+ b 这三个操作,第一步(a=1)和第二步 (b=2) 因为不存在数据依赖关系,所以可能会发

    生重排序,然而 c =a+ b 这个操作是不会被重排序的,因为须要保障最终的后果肯定是 c =a+b=3。

    重排序在单线程下肯定能保障后果的正确性,然而在多线程环境下,可能产生重排序,影响后果,下例中的 1 和 2 因为不存在数据依赖关系,则有可能会被重排序,先执行 status=true 再执行 a =2。而此时线程 B 会顺利达到 4 处,而线程 A 中 a = 2 这个操作还未被执行,所以 b =a+ 1 的后果也有可能仍然等于 2。

指令重排序

出于性能起因容许 JVM 和 CPU 从新排序程序中的指令,只有指令的语义含意放弃不变即可。例如,查看上面的指令:

COPYint a = 1;
int b = 2;

a++;
b++;

这些指令能够按以下程序从新排序,而不会失落程序的语义含意:

COPYint a = 1;
a++;

int b = 2;
b++;

然而,当其中一个变量是 volatile 变量时,指令重排序会呈现一个挑战。让咱们看看 MyClass 这个后面 Java volatile 教程中的例子中呈现的类:

COPYpublic class MyClass {
    private int years;
    private int months
    private volatile int days;

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

一旦 update() 办法写入一个值 days,新写入的值,以yearsmonths也被写入主存储器。然而,如果 JVM 从新排序指令,如下所示:

COPYpublic void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}

days 变量被批改时 monthsyears的值依然写入主内存中,然而这一次它产生在新的值被写入 monthsyears之前,也就是这两个变量的旧值会写入主存中,前面两句的写入操作只是写到缓存中。因而,新值不能正确地对其余线程可见。从新排序的指令的语义含意曾经扭转。

happens before

下面讲的是 volatile 变量本身的个性,对程序员来说,volatile 对线程的内存可见性的影响比 volatile 本身的个性更为重要,也更须要咱们去关注。

从 JSR-133 开始,volatile 变量的写 - 读能够实现线程之间的通信。

从内存语义的角度来说,volatile 与锁有雷同的成果:volatile 写和锁的开释有雷同的内存语义;volatile 读与锁的获取有雷同的内存语义。

请看上面应用 volatile 变量的示例代码:

COPYclass VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;               //2
    }

    public void reader() {if (flag) {                //3
            int i =  a;           //4
            ……
        }
    }
}

假如线程 A 执行 writer()办法之后,线程 B 执行 reader()办法。依据 happens before 规定,这个过程建设的 happens before 关系能够分为两类:

  1. 依据程序秩序规定,1 happens before 2; 3 happens before 4。
  2. 依据 volatile 规定,2 happens before 3。
  3. 依据 happens before 的传递性规定,1 happens before 4。

上述 happens before 关系的图形化表现形式如下:

上图中,每一个箭头链接的两个节点,代表了一个 happens before 关系。彩色箭头示意程序程序规定;橙色箭头示意 volatile 规定;蓝色箭头示意组合这些规定后提供的 happens before 保障。

这里 A 线程写一个 volatile 变量后,B 线程读同一个 volatile 变量。A 线程在写 volatile 变量之前所有可见的共享变量,在 B 线程读同一个 volatile 变量后,将立刻变得对 B 线程可见。

Happens-Before 保障

为了解决指令重排序挑战,除了可见性保障之外,Java volatile关键字还提供“happens-before”保障。happens-before 保障保障:

volatile 之前读写

如果读取 / 写入最后产生在写入 volatile 变量之前,读取 / 写入其余变量不能从新排序在写入 volatile 变量之后。
​ 写入 volatile 变量之前的读 / 写操作被保障“happen before”写入 volatile 变量。请留神,产生在写入 volatile 变量之后的读 / 写操作仍然能够重排序到写入 volatile 变量前,只是不能相同。容许从后到前,但不容许从前到后。

volatile 之后读写

如果读 / 写操作最后产生在读取 volatile 变量之后,则读取 / 写入其余变量不能重排序到产生在读取 volatile 变量之前。请留神,产生在读取 volatile 变量之前的读 / 写操作仍然能够重排序到读取 volatile 变量后,只是不能相同。容许从前到后,但不容许从后到前。

上述“happens-before”规定保障确保 volatile 关键字的可见性保障在强制执行。

COPYpublic class VolatileTest {
    private volatile int vi = 1;
    private int i = 2;
    private int i2 = 3;

    @Test
    public void test() {System.out.println(i);      //1  读取一般变量
        i=3;                        //2  写入一般变量

        //1 2 不能重排序到 3 之后,操作 4 能够重排序到 3 后面
        vi = 2;                     //3  写入 volatile 变量
        i2 = 5;                     //4  写入一般变量
    }

    @Test
    public void test2() {System.out.println(i);      //1  读取一般变量

        // 3 不能重排序到在 2 前,但 1 能够重排序到 2 后
        System.out.println(vi);     //2  读取 volatile 变量
        System.out.println(i2);     //3  读取一般变量
    }
}

volatile 注意事项

volatile 线程不平安

即便 volatile 关键字保障 volatile 变量的所有读取间接从主存储器读取,并且所有对 volatile 变量的写入都间接写入主存储器,依然存在申明 volatile 变量线程不平安。

在后面解释的状况中,只有线程 1 写入共享 counter 变量,申明 counter 变量为 volatile 足以确保线程 2 始终看到最新的写入值。

实际上,如果写入 volatile 变量的新值不依赖于其先前的值,则甚至能够多个线程写入共享变量,并且依然能够在主存储器中存储正确的值。换句话说,就是将值写入共享 volatile 变量的线程开始并不需要读取其旧值来计算其下一个值。

一旦线程须要首先读取 volatile 变量的旧值,并且基于该值为共享 volatile 变量生成新值,volatile变量就不再足以保障正确的可见性。读取 volatile 变量和写入新值之间的短时间距离会产生竞争条件,其中多个线程可能读取volatile 变量的同一个旧值,而后为其生成新值,并将该值写回主内存 – 笼罩彼此的值。

多个线程递增同一个计数器的状况正是 volatile变量并不平安的状况。以下局部更具体地解释了这种状况。

设想一下,如果线程 1 将值为 0 的共享变量 counter 读入其 CPU 高速缓存,将其减少到 1 并且不将更改的值写回主存储器。而后,线程 2 也从主存储器读取雷同的 counter 变量进入本人的 CPU 高速缓存,其中变量的值仍为 0。而后,线程 2 也将计数器递增到 1,也不将其写回主存储器。这种状况如下图所示:

线程 1 和线程 2 当初失去了同步。共享变量 counter 的理论值应为 2,但每个线程的 CPU 缓存中的变量值为 1,而在主内存中,该值仍为 0。这是一个凌乱!即便线程最终将共享变量 counter 的值写回主存储器,该值也将是谬误的。

保障线程平安

正如我后面提到的,如果两个线程都在读取和写入共享变量,那么应用 volatile关键字是不平安的。在这种状况下,您须要应用 synchronized 来保障变量的读取和写入是原子性的。读取或写入一个 volatile 变量不会阻塞其余线程读取或写入这个变量。为此,您必须在临界区四周应用 synchronized 关键字。

作为 synchronized 块的代替办法,您还能够应用 java.util.concurrent 包中泛滥的原子数据类型。例如,AtomicLong或者 AtomicReference或其余的。

如果只有一个线程读取和写入 volatile 变量的值,而其余线程只读取这个变量,那么此线程将保障其余线程能看到 volatile 变量的最新值。如果不将变量申明为volatile,则无奈保障。

volatile关键字也能够保障在 64 位变量上失常应用。

volatile 的性能思考

读取和写入 volatile 变量会导致变量从主存中读取或写入主存,读取和写入主内存比拜访 CPU 缓存开销更大。拜访 volatile 变量也会阻止指令重排序,这是一种失常的性能晋升技术。因而,当您的确须要强制施行变量可见性时,才应用 volatile 变量。

原理

volatile 能够保障线程可见性且提供了肯定的有序性,然而无奈保障原子性。在 JVM 底层 volatile 是采纳“内存屏障”来实现的。察看退出 volatile 关键字和没有退出 volatile 关键字时所生成的汇编代码发现,退出 volatile 关键字时,会多出一个 lock 前缀指令,lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个性能:

  • 它确保指令重排序时不会把其前面的指令排到内存屏障之前的地位,也不会把后面的指令排到内存屏障的前面;即在执行到内存屏障这句指令时,在它后面的操作曾经全副实现;
  • 它会强制将对缓存的批改操作立刻写入主存;
  • 如果是写操作,它会导致其余 CPU 中对应的缓存行有效。

内存语义

volatile 写的内存语义

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

以下面示例程序 VolatileExample 为例,假如线程 A 首先执行 writer()办法,随后线程 B 执行 reader()办法,初始时两个线程的本地内存中的 flag 和 a 都是初始状态。下图是线程 A 执行 volatile 写后,共享变量的状态示意图:

如上图所示,线程 A 在写 flag 变量后,本地内存 A 中被线程 A 更新过的两个共享变量的值被刷新到主内存中。此时,本地内存 A 和主内存中的共享变量的值是统一的。

volatile 读的内存语义

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为有效。线程接下来将从主内存中读取共享变量。

上面是线程 B 读同一个 volatile 变量后,共享变量的状态示意图:

如上图所示,在读 flag 变量后,本地内存 B 曾经被置为有效。此时,线程 B 必须从主内存中读取共享变量。线程 B 的读取操作将导致本地内存 B 与主内存中的共享变量的值也变成统一的了。

如果咱们把 volatile 写和 volatile 读这两个步骤综合起来看的话,在读线程 B 读一个 volatile 变量后,写线程 A 在写这个 volatile 变量之前所有可见的共享变量的值都将立刻变得对读线程 B 可见。

小结

上面对 volatile 写和 volatile 读的内存语义做个总结

  • 线程 A 写一个 volatile 变量,本质上是线程 A 向接下来将要读这个 volatile 变量的某个线程收回了(其对共享变量所在批改的)音讯。
  • 线程 B 读一个 volatile 变量,本质上是线程 B 接管了之前某个线程收回的(在写这个 volatile 变量之前对共享变量所做批改的)音讯。
  • 线程 A 写一个 volatile 变量,随后线程 B 读这个 volatile 变量,这个过程本质上是线程 A 通过主内存向线程 B 发送音讯。

volatile 内存语义的实现

前文咱们提到过重排序分为编译器重排序和处理器重排序。为了实现 volatile 内存语义,JMM 会别离限度这两种类型的重排序类型。上面是 JMM 针对编译器制订的 volatile 重排序规定表:

是否能重排序 第二个操作 第二个操作 第二个操作
第一个操作 一般读 / 写 volatile 读 volatile 写
一般读 / 写 NO
volatile 读 NO NO NO
volatile 写 NO NO

举例来说,第三行最初一个单元格的意思是:在程序程序中,当第一个操作为一般变量的读或写时,如果第二个操作为 volatile 写,则编译器不能重排序这两个操作。

从上表咱们能够看出

  • 当第二个操作为 volatile 写操作时, 不论第一个操作是什么(一般读写或者 volatile 读写), 都不能进行重排序。这个规定确保 volatile 写之前的所有操作都不会被重排序到 volatile 写之后;
  • 当第一个操作为 volatile 读操作时, 不论第二个操作是什么, 都不能进行重排序。这个规定确保 volatile 读之后的所有操作都不会被重排序到 volatile 读之前;
  • 当第一个操作是 volatile 写操作时, 第二个操作是 volatile 读操作, 不能进行重排序。

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。上面是基于激进策略的 JMM 内存屏障插入策略:

  • 在每个 volatile 写操作的后面插入一个 StoreStore 屏障(禁止后面的写与 volatile 写重排序)。
  • 在每个 volatile 写操作的前面插入一个 StoreLoad 屏障(禁止 volatile 写与前面可能有的读和写重排序)。
  • 在每个 volatile 读操作的前面插入一个 LoadLoad 屏障(禁止 volatile 读与前面的读操作重排序)。
  • 在每个 volatile 读操作的前面插入一个 LoadStore 屏障(禁止 volatile 读与前面的写操作重排序)。

其中重点说下 StoreLaod 屏障,它是确保可见性的要害,因为它会将屏障之前的写缓冲区中的数据全副刷新到主内存中。上述内存屏障插入策略十分激进,但它能够保障在任意解决平台,任意的程序中都能失去正确的 volatile 语义。上面是激进策略(为什么说激进呢,因为有些在理论的场景是可省略的)下,volatile 写操作 插入内存屏障后生成的指令序列示意图:

其中 StoreStore 屏障能够保障在 volatile 写之前,其后面的所有一般写操作对任意处理器可见(把它刷新到主内存)。

另外 volatile 写前面有 StoreLoad 屏障,此屏障的作用是防止 volatile 写与前面可能有的读或写操作进行重排序。因为编译器经常无奈精确判断在一个 volatile 写的前面是否须要插入一个 StoreLoad 屏障(比方,一个 volatile 写之后办法立刻 return)为了保障能正确实现 volatile 的内存语义,JMM 采取了激进策略:在每个 volatile 写的前面插入一个 StoreLoad 屏障。因为 volatile 写 - 读内存语义的常见模式是:一个写线程写 volatile 变量,多个度线程读同一个 volatile 变量。当读线程的数量大大超过写线程时,抉择在 volatile 写之后插入 StoreLoad 屏障将带来可观的执行效率的晋升。从这里也可看出 JMM 在实现上的一个特点:首先确保正确性,而后再去谋求效率(其实咱们工作中编码也是一样)。

上面是在激进策略下,volatile 读插入内存屏障后生产的指令序列示意图:

上述 volatile 写和 volatile 读的内存屏障插入策略十分激进。在理论执行时,只有不扭转 volatile 写 - 读的内存语义,编译器能够依据具体情况疏忽不必要的屏障。在 JMM 根底中就有提到过各个处理器对各个屏障的反对度,其中 x86 处理器仅会对写 - 读操作做重排序。

上面咱们通过具体的示例代码来阐明

COPYclass VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           // 第一个 volatile 读
        int j = v2;           // 第二个 volatile 读
        a = i + j;            // 一般写
        v1 = i + 1;          // 第一个 volatile 写
        v2 = j * 2;          // 第二个 volatile 写
    }

    …                    // 其余办法
}

针对 readAndWrite() 办法,编译器在生成字节码时能够做如下的优化:

留神,最初的 StoreLoad 屏障不能省略。因为第二个 volatile 写之后,办法立刻 return。此时编译器可能无奈精确判定前面是否会有 volatile 读或写,为了平安起见,编译器经常会在这里插入一个 StoreLoad 屏障。

下面的优化是针对任意处理器平台,因为不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还能够依据具体的处理器内存模型持续优化。以 x86 处理器为例,上图中除最初的 StoreLoad 屏障外,其它的屏障都会被省略。

后面激进策略下的 volatile 读和写,在 x86 处理器平台能够优化成:

前文提到过,x86 处理器仅会对写 – 读操作做重排序。,x86 处理器仅会对写 - 读操作做重排序。X86 不会对读 - 读,读 - 写和写 - 写操作做重排序,因而在 x86 处理器中会省略掉这三种操作类型对应的内存屏障。在 x86 中,JMM 仅需在 volatile 写前面插入一个 StoreLoad 屏障即可正确实现 volatile 写 - 读的内存语义。这意味着在 x86 处理器中,volatile 写的开销比 volatile 读的开销会大很多(因为执行 StoreLoad 屏障开销会比拟大)。

为什么要加强 volatile 的内存语义

在 JSR-133 之前的旧 Java 内存模型中,尽管不容许 volatile 变量之间重排序,但旧的 Java 内存模型容许 volatile 变量与一般变量之间重排序。在旧的内存模型中,VolatileExample 示例程序可能被重排序成下列时序来执行:

在旧的内存模型中,当 1 和 2 之间没有数据依赖关系时,1 和 2 之间就可能被重排序(3 和 4 相似)。其后果就是:读线程 B 执行 4 时,不肯定能看到写线程 A 在执行 1 时对共享变量的批改。

因而在旧的内存模型中,volatile 的写 - 读没有锁的开释 - 获所具备的内存语义。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133 专家组决定加强 volatile 的内存语义:严格限度编译器和处理器对 volatile 变量与一般变量的重排序,确保 volatile 的写 - 读和锁的开释 - 获取一样,具备雷同的内存语义。从编译器重排序规定和处理器内存屏障插入策略来看,只有 volatile 变量与一般变量之间的重排序可能会毁坏 volatile 的内存语意,这种重排序就会被编译器重排序规定和处理器内存屏障插入策略禁止。

因为 volatile 仅仅保障对单个 volatile 变量的读 / 写具备原子性,而锁的互斥执行的个性能够确保对整个临界区代码的执行具备原子性。在性能上,锁比 volatile 更弱小;在可伸缩性和执行性能上,volatile 更有劣势。如果读者想在程序中用 volatile 代替监视器锁,请肯定审慎,具体细节请参阅参考 Java 实践与实际:正确应用 Volatile 变量。

本文由 传智教育博学谷狂野架构师 教研团队公布。

如果本文对您有帮忙,欢送 关注 点赞 ;如果您有任何倡议也可 留言评论 私信,您的反对是我保持创作的能源。

转载请注明出处!

正文完
 0