BiBi - 并发编程 -2- volatile

From:Java并发编程的艺术

  • 目录
    BiBi - 并发编程 -0- 开篇
    BiBi - 并发编程 -1- 挑战
    BiBi - 并发编程 -2- volatile
    BiBi - 并发编程 -3- 锁
    BiBi - 并发编程 -4- 原子操作
    BiBi - 并发编程 -5- Java内存模型
    BiBi - 并发编程 -6- final关键字
    BiBi - 并发编程 -7- DCL
    BiBi - 并发编程 -8- 线程
    BiBi - 并发编程 -9- ReentrantLock
    BiBi - 并发编程 -10- 队列同步器
    BiBi - 并发编程 -11- 并发容器
    BiBi - 并发编程 -12- Fork/Join框架
    BiBi - 并发编程 -13- 并发工具类
    BiBi - 并发编程 -14- 线程池
    BiBi - 并发编程 -15- Executor框架

1. volatile简介

volatile是轻量级的,保证“可见性”,体现高手的水平,因为它不会引起线程上下文的切换和调度,比synchronized执行成本更低。应用场景:只有一个线程对变量进行写操作,其它线程对该变量只进行读操作。
注:ThreadLocal的应用在某种程度上也可以实现并发。

2. volatile实现可见性的本质

1)volatile修饰的变量会生成一个Lock前缀的指令,Lock前缀指令会引起处理器缓存会写到内存。
2)一个处理器的缓存写到内存会导致其他处理器的缓存无效。采用缓存一致协议,每个处理器通过嗅探在总线程上传播数据来检查自己的缓存值是否过期。缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存数据。

用【缓存锁定】代替【总线锁定】来提高效率,锁住总线会导致其它CPU不能访问总线。

嗅探技术:保证处理器内部缓存和系统内存的数据在总线上保持一致。当嗅探到其它处理器在写内存地址,而这个地址处于共享状态,那么正在嗅探的处理器将自己的缓存设为无效,下次访问相同地址时,强制执行缓存行填充,即从主内存中重新读取。

3. volatile优化LinkedTransferQueue

背景:处理器的高速缓存行是64个字节宽,不支持部分填充缓存行。如果队列的头结点和尾结点都不足64字节,导致处理器将他们读到同一个高速缓存行中。
问题:当一个处理器修改头结点时,由于锁定的缓存行中同时包含头结点和尾结点,在缓存一致性机制作用下,导致其它处理器不能访问自己高速缓存中的尾结点。
优化:通过追加字节的巧妙方法,将共享变量【一个对象引用占4个字节】追加到64字节再填充到缓存行,避免头结点和尾结点在同一缓存行,使头结点和尾结点在修改时不会互相锁定,从而提高队列的入队和出队效率。

4. volatile特性

原子性:任意单个volatile变量的读写具有原子性,如:a = 1; return b; c = 10L,其中对于64位的long/double的写也具有原子性。但对于多个volatile操作或volatile复合操作,如:i++,不具有原子性。
可见性:对一个volatile变量的读,总是能看到对这个volatile变量最后的写入。

volatile可见性应用的例子

不仅仅是让volatile修饰的变量对其它线程可见,而是所有可见的共享变量。

class 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规则,操作1 happens-before 操作4,因此可以正常运行。volatile的写-读与锁的释放-获取具有相同的内存效果,在线程B读一个volatile变量后,线程A在写这个volatile变量之前所有可见的共享变量的值都将立刻变得对线程B可见,即操作2执行完成后,会把共享变量 a 和 flag 刷新到主内存。

1)线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出【对共享变量所做的修改的】消息。如:上述中的 a = 1。
2)线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的【在写这个volatile变量之前对共享变量所做修改的】消息。如:上述中的 a = 1。

根据volatile重排序规则可知:操作 1 和操作 2 不会重排序;操作 3 和操作 4 不会重排序。

4. volatile的重排序

volatile重排序规则表

5. volatile的内存屏障

为了实现volatile内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。【两个volatile读/写语句不能重排序】
1)在每个volatile写操作的面插入一个StoreStore屏障。禁止上面的普通写和下面的volatile写重排序。
StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。
2)在每个volatile写操作的面插入一个StoreLoad屏障。
StoreLoad避免volatile写与后面可能有的volatile读/写操作重排序。StoreLoad可以在每个volatile写后面,也可以在每个volatile读前面进行插入,但从整体效率角度考虑,常见的使用模式:一个线程写volatile变量,多个线程读volatile变量。所以JVM最终选择的是在每个volatile写后面插入StoreLoad屏障,
3)在每个volatile读操作的面插入一个LoadLoad屏障。
LoadLoad禁止下面所有的普通读操作和上面的volatile读重排序。
4)在每个volatile读操作的面插入一个LoadStore屏障。
LoadStore禁止下面所有的普通写操作和上面的volatile读重排序。

本质:volatile写禁止上面的的操作和它重排序,volatile读禁止下面的操作和它重排序。并且volatile写还禁止它和下面的volatile读/写重排序。

volatile写
volatile读
具体例子
class 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写
    }
 
    …                         // 其他方法
}

编译器在生成字节码时的优化。


5. volatile在ReentrantLock中的应用

ReentrantLock的实现依赖于Java同步框架AbstractQueuedSynchronizer【AQS】,AQS使用一个整型的volatile变量state来维护同步状态。获取锁的开始,首先读取volatile变量state;释放锁的最后,写volatile变量state。CAS具有volatile读/写的内存语义,编译器不能对CAS与CAS前面和后面的意内存操作重排序。可见,ReentrantLock利用了volatile变量和CAS的读/写所具有的内存语义。

你可能感兴趣的:(BiBi - 并发编程 -2- volatile)