定义

volatile是Java并发编程中的一个关键字,可以用来修饰变量。被它所修饰的变量,会拥有可见性以及原子性(针对单一读写而言)。

可见性

对于volatile变量而言,一个线程对其进行修改后所有线程都能读到修改后的值。

原子性

对volatile变量的一次读和写是原子性的。例如Java中对于64位的doublelong写是分两次进行的,第一次写高32位,第二次写低32位,如果不加限制,可能会读到中间状态的值,称为字撕裂。volatile修饰这类变量可以保证原子性。但是对多个读写操作并不是原子性,例如:

1
2
3
4
5
6
7
volatile int i = 1;
i = i++;
//等价于
int temp = i; //1
temp = temp + 1; //2
i = temp; //3
//步骤1和3之间,i的值可能被其他线程所改变,所以对于多次读写不具有原子性。

内存可见性

volatile写的内存语义:当写一个volatile变量时,JVM会把该线程对应的本地内存中的共享变量刷新到主内存。
volatile读的内存语义:当读一个volatile变量时,JVM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
综合这两个步骤,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。即:

  • 线程A写一个volatile变量,实际上是线程A向接下来要读这个volatile变量的某个线程发出了(其对共享变量所作修改的)信息。
  • 线程B读一个volatile变量,实际上是线程B接受了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
  • 对volatile变量的写,相当于锁的释放;对volatile变量的读,相当于锁的获取。

实现

介绍实现之前,要先介绍几个概念:

重排序

执行程序时,为提高效率,编译器和处理器常常会对指令做重排序。
重排序不会改变存在数据依赖关系的两个操作的执行顺序
重排序分三种类型:

  1. 编译器优化的重排序
  2. 指令集并行的重排序
  3. 内存系统的重排序

由于现代处理器都会使用写缓冲区,因此现代的处理器都会允许写-读操作进行重排序。例如程序首先写一个变量a,然后读取一个变量b。但由于写缓冲区的存在,a并没有被写入内存,而是写到了本地缓存。读取b之后将a的值刷新主存,就是发生了写-读重排序。

所以为了保证内存可见性,需要禁止某些重排序。通过在适当位置插入内存屏障来实现。内存屏障有四种:LoadLoadBarriersStoreStoreBarriersLoadStoreBarriersStoreLoadBarriers

happens-before简介

数据依赖

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时两个操作之间就存在数据依赖性。存在数据依赖性的两个操作进行重排序,程序的执行结果就会被改变。

as-if-serial语义

不管怎么重排序,(单线程)程序的执行结果不能被改变。

数据竞争

  1. 在一个线程中写一个变量,
  2. 在另一个线程中读同一个变量,
  3. 而且写和读没有通过同步来排序。
    当代码中包含数据竞争,程序的执行往往会产生反直觉的效果。通过多线程同步,可以消除数据竞争。

happens-before定义

happens-before用来定义分布式系统中事件之间的偏序关系。两个操作可以在一个线程之内,也可以在不同线程之间。因此JMM通过happens-before向程序员提供跨线程的内存可见性保证。happens-before关系定义如下:

  1. 如果一个操作happens-before于另一个操作,那么第一个操作的结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须按照happens-before关系指定的顺序执行。也就是说,如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法。

规则1是JMM对程序员的承诺,即如果A happens-before B,那么JMM将向程序员保证A操作的结果对B可见,且A的执行顺序排在B之前。

规则2是JMM对编译器和处理器重排序的约束原则。即只要不改变程序的执行结果,(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。原因是:程序员并不关心程序是否重排序,程序员只关心程序执行时的语义不能被改变。

happens-before规则

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程的任意后续操作。
  2. 监视器锁规则:对一个线程的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性。
  5. start()规则:如果线程A执行操作ThreadB.start(),那么线程A的ThreadB.start()操作happens-before于线程B中的任意操作。即:线程A在执行start()前对共享变量的修改,B都是可见的。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于A从ThreadB.join()操作成功返回。即:线程B的修改,在join()返回后对A都是可见的。

volatile内存语义的实现

通过限制重排序来实现内存语义,具体的规则如下:

  1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写操作之后。
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读操作之前。
  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
    编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。基于保守策略的JMM内存插入策略:
    • 在每个volatile写操作之前,插入StoreStore
    • 在每个volatile写操作之后,插入StoreLoad
    • 在每个volatile读操作之后,插入LoadLoad
    • 在每个volatile读操作之后,插入LoadStore

总结

volatile可以说是Java中最轻量级的锁,但在使用时也有很多要注意的地方。对于Java内存模型的描述还很粗浅,有需要应该更深入理解。对volatile的理解是这几天读书学到的,可能有些地方会有误解或错误,以后应继续学习并完善这方面的知识。