Volatile关键字

原文链接: http://tutorials.jenkov.com/java-concurrency/volatile.html

文章目录

  • Volatile关键字说明
  • 可见性
    • 变量的可见性问题
    • Java volatile可见性保证
    • 完整volatile可见性保证
  • 禁止指令重排序
    • 指令重新排序的挑战
  • Java volatile关键字的Happens-Before保证
  • volatile使用时的注意事项
    • volatile并不总是足够的
    • 什么时候使用volatile是足够的
    • volatile关键字性能方面的考虑

Volatile关键字说明

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

  实际上,由于Java 5中的volatile关键字保证了volatile变量被写入主内存并从主内存中读取

可见性

变量的可见性问题

  Java volatile关键字保证跨线程更改变量的可见性。在线程操作非volatile变量的多线程应用程序中,出于性能原因,每个线程在处理变量时,都可以将变量从主内存复制到CPU缓存中。如果您的计算机包含多个CPU,每个线程可能运行在不同的CPU上。这意味着,每个线程可以将变量复制到不同CPUCPU缓存中。如下图所示:

Volatile关键字_第1张图片

  对于非volatile变量,不能保证Java虚拟机(JVM)何时将数据从主内存读入CPU缓存,或何时将数据从CPU缓存写入主内存。这可能会导致几个问题:

  假设有这样一种情况,两个或多个线程访问一个包含计数器变量的共享对象,该对象声明如下:

public class SharedObject {

    public int counter = 0;

}

  再想象一下,只有线程1递增计数器变量,但实际是线程1和线程2可能会不时读取计数器变量。

  如果计数器变量没有声明为volatile,则无法保证计数器变量的值何时从CPU缓存写入主内存。这意味着,CPU缓存中的计数器变量值可能与主内存中的不一样。这种情况说明如下:

Volatile关键字_第2张图片

  由于变量的最新值还没有被另一个线程写回主内存,所以线程看不到该变量的最新值,这个问题称为“可见性”问题。一个线程的更新对其他线程不可见。

Java volatile可见性保证

  Java volatile关键字的目的是解决可变的可见性问题。通过声明计数器变量volatile,对计数器变量的所有写操作都将立即被写回主内存。此外,计数器变量的所有读取都将直接从主内存中读取。

  下面是计数器变量的volatile声明:

public class SharedObject {

    public volatile int counter = 0;

}

  因此,声明一个变量volatile可以保证其他线程对该变量的写操作的可见性。

  在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改计数器),声明计数器变量volatile足以保证T2对计数器变量的写操作的可见性。

  但是,如果T1T2都在增加计数器变量,那么仅仅声明计数器变量volatile是不够的。

完整volatile可见性保证

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

  • 如果线程A写入一个volatile变量,而线程B随后读取相同的volatile变量,那么线程A在写入volatile变量之前可以看到的所有变量,在线程B读取volatile变量之后也可以看到。
  • 如果线程A读取volatile变量,那么线程A在读取volatile变量时可见的所有变量也将从主内存中重新读取。

  让我用一个代码例子来说明:

public 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时,线程可见的所有变量也被写入主内存。这意味着,当一个值被写入days时,yearsmonths的值也被写入主内存。

  当你读yearsmonthsdays的值时,你可以这样做:

public 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 VMCPU出于性能原因对程序中的指令进行重新排序。例如:

int a = 1;
int b = 2;

a++;
b++;

  这些指令可以重新排序到以下顺序,而不会失去程序的语义:

int a = 1;
a++;

int b = 2;
b++;

  然而,当其中一个变量是volatile变量时,指令重新排序就会带来挑战。让我们看看这个Java volatile教程前面例子中的MyClass类:

public 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,那么新写入的值也将写入主内存。但是,如果Java VM像这样重新排序指令会怎么样呢?

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

  在修改days变量时,monthyears的值仍然写入主内存,但这一次是在将新值写入monthyears之前。因此,新值对其他线程不可见。重新排序的指令的语义发生了变化。

Java volatile关键字的Happens-Before保证

  为了解决指令重新排序的挑战,Java volatile关键字除了提供可视性保证之外,还提供了一个“happens-before”保证。happens-before保证担保如下:

  • 如果对volatile变量的读/写最初发生在对volatile变量的写之前,那么对其他变量的读/写不能在对volatile变量的写之后重新排序。对volatile变量的写之前的读/写保证在对volatile变量的写之前发生。请注意,仍然有可能,例如,对volatile的写操作之后的其他变量的读/写操作被重新排序,以在对volatile的写操作之前执行。只是不是反过来。从后到前是允许的,但从前到后是不允许的。

  • 如果对其他变量的读/写最初发生在volatile变量的读/写之后,则不能在volatile变量的读/写之前重新排序。请注意,在读取volatile变量之前发生的其他变量的读取可以重新排序为在读取volatile变量之后发生的读取。只是不是反过来。从前到后是允许的,但从后到前是不允许的。

  上面的happens-before保证确保了volatile关键字的可见性得到正确的执行。

volatile使用时的注意事项

volatile并不总是足够的

  即使volatile关键字保证volatile变量的所有读操作都直接从主内存中读取,并且对volatile变量的所有写操作都直接写到主内存中,仍然存在这样的情况,即声明一个变量volatile是不够的。

  在前面解释的只有线程1写入共享计数器变量的情况下,声明计数器变量volatile就足以确保线程2始终看到最新的写入值。

  事实上,如果写入变量的新值不依赖于其先前的值,那么多个线程甚至可以写入共享的volatile变量,并且仍然在主内存中存储正确的值。换句话说,如果向共享volatile变量写入值的线程不需要首先读取它的值来计算它的下一个值。

  只要线程需要首先读取volatile变量的值,并且基于该值为共享volatile变量生成一个新值,那么volatile变量就不再足以保证正确的可见性。从读取volatile变量到写入其新值之间的短时间间隔,创建一个竞态条件,其中多个线程可能读取同一个volatile变量的值,为变量生成一个新值,并在将该值写回主内存时——覆盖彼此的值。

  多线程递增同一个计数器的情况正好是volatile变量不够用的情况。

  想象一下,如果线程1将一个值为0的共享计数器变量读入其CPU缓存,将其增加到1,而不将更改后的值写回主内存。然后,线程2可以从主内存(变量的值仍然为0)中读取相同的计数器变量,并将其读入自己的CPU缓存。线程2也可以将计数器增加到1,但也不能将它写回主内存。如下图所示:

Volatile关键字_第3张图片

  线程1和线程2现在实际上是不同步的。共享计数器变量的实际值应该是2,但是每个线程的CPU缓存中变量的值都是1,在主内存中值仍然是0。即使线程最终将共享计数器变量的值写回主内存,该值也将是错误的。

什么时候使用volatile是足够的

  正如我前面提到的,如果两个线程同时读写一个共享变量,那么仅使用volatile关键字是不够的。在这种情况下,需要使用synchronized来确保变量的读写是原子性的。读取或写入volatile变量不会阻塞线程的读取或写入。要实现这一点,必须在关键部分周围使用synchronized关键字。

  作为synchronized代码块的替代,您还可以使用java.util.concurrent包中的许多原子数据类型。例如,AtomicLongAtomicReference或者其它。

  如果只有一个线程读写volatile变量的值,而其他线程只读取变量,那么读取线程将确保看到写入volatile变量的最新值。如果不使用volatile标记该变量,就不能保证这一点。

  volatile关键字保证可以处理32位和64个变量。

volatile关键字性能方面的考虑

  volatile变量的读取和写入将导致变量被读取或写入主内存。从主存读取和写入比访问CPU缓存更昂贵。访问volatile变量还可以防止指令重新排序,这是一种常见的性能增强技术。因此,您应该只在真正需要强制变量可见性时才使用volatile变量。

你可能感兴趣的:(高并发,高并发)