关于Volatile(一)

一.Volatile是什么

Java语言规范第三版中对volatile的定义如下: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。

Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。

在JVM底层volatile是采用”内存屏障”来实现的。即一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。保证可见性、不保证原子性。

(2)禁止进行指令重排序。

在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。

二.Volatile能做什么

1、为什么要使用Volatile

Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。

Volatile是轻量级的synchronized。

三.Volatile原理

1、可见性实现:

Volatile是如何来保证可见性的呢?在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,

因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

当一个变量定义为 volatile 之后,将具备两种特性:

(1)保证此变量对所有的线程的可见性,这里的“可见性”,是指当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。

但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。

(2)禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),

只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因。

因此要实现volatile变量的可见性,直接从这方面入手即可。对volatile变量的写操作与普通变量的主要区别有两点:

(1)修改volatile变量时会强制将修改后的值刷新的主内存中。

(2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

通过这两个操作,就可以解决volatile变量的可见性问题。

2、有序性实现:

在解释这个问题前,先来了解一下Java中的happen-before规则,JSR 133中对Happen-before的定义如下:

Two actions can be ordered by a happens-before relationship.If one action happens before another, then the first is visible to and ordered before the second.

通俗一点说就是如果a happen-before b,则a所做的任何操作对b是可见的。(这一点大家务必记住,因为happen-before这个词容易被误解为是时间的前后)。

我们再来看看JSR 133中定义了哪些happen-before规则:

• Each action in a thread happens before every subsequent action in that thread.
• An unlock on a monitor happens before every subsequent lock on that monitor.
• A write to a volatile field happens before every subsequent read of that volatile.
• A call to start() on a thread happens before any actions in the started thread.
• All actions in a thread happen before any other thread successfully returns from a join() on that thread.
• If an action a happens before an action b, and b happens before an action c, then a happens before c.

翻译过来为:

(1)同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。

(2)监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)

(3)对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)

(4)线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)

(5)线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。

(6)如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

这里主要看下第三条:volatile变量的保证有序性的规则。《Java并发编程:核心理论》一文中提到过重排序分为编译器重排序和处理器重排序。

为了实现volatile内存语义,JMM会对volatile变量限制这两种类型的重排序。下面是JMM针对volatile变量所规定的重排序规则表:

3、内存屏障

为了实现volatile可见性和happen-befor的语义。JVM底层是通过一个叫做“内存屏障”的东西来完成。内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。

下面是完成上述规则所要求的内存屏障:

(1)LoadLoad 屏障

执行顺序:Load1—>Loadload—>Load2

确保Load2及后续Load指令加载数据之前能访问到Load1加载的数据。

(2)StoreStore 屏障

执行顺序:Store1—>StoreStore—>Store2

确保Store2以及后续Store指令执行前,Store1操作的数据对其它处理器可见。

(3)LoadStore 屏障

执行顺序: Load1—>LoadStore—>Store2

确保Store2和后续Store指令执行前,可以访问到Load1加载的数据。

(4)StoreLoad 屏障

执行顺序: Store1—> StoreLoad—>Load2

确保Load2和后续的Load指令读取之前,Store1的数据对其他处理器是可见的。

最后我可以通过一个实例来说明一下JVM中是如何插入内存屏障的:

package com.paddx.test.concurrent;

public class MemoryBarrier {
    int a, b;
    volatile int v, u;

    void f() {
        int i, j;

        i = a;
        j = b;
        i = v;
        //LoadLoad
        j = u;
        //LoadStore
        a = i;
        b = j;
        //StoreStore
        v = i;
        //StoreStore
        u = j;
        //StoreLoad
        i = u;
        //LoadLoad
        //LoadStore
        j = b;
        a = i;
    }
}

四.volatile的适用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,

但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

通常来说,使用volatile必须具备以下2个条件:

(1)对变量的写操作不依赖于当前值;

(2)该变量没有包含在具有其他变量的不变式中;

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

并发专家建议我们远离volatile是有道理的,这里再总结一下:

(1)volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大。

(2)如今非volatile的共享变量,在访问不是超级频繁的情况下,已经和volatile修饰的变量有同样的效果了。

(3)volatile不能保证原子性,这点是大家没太搞清楚的,所以很容易出错。

(4)volatile可以禁止重排序。

所以如果我们确定能正确使用volatile,那么在禁止重排序时是一个较好的使用场景,否则我们不需要再使用它。

这里只列举出一种volatile的使用场景,即作为标识位的时候(比如本文例子中boolean类型的flag)。

用专业点更广泛的说法就是“对变量的写操作不依赖于当前值且该变量没有包含在其他具体变量的不变式中”。

原文地址:https://www.cnblogs.com/ZJOE80/p/12870997.html