volatile关键字解读

  volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略

volatile 的特性

  • 保证共享变量的可见性:使用volatile修饰的变量,任何线程对其进行操作都是在主内存中进行的,不会产生副本,从而保证共享变量的可见性。
  • 防止局部指令重排序:happens-before规则中的volatile变量规则规定了一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性,看成是使用同一个锁对这些单个读/写操作做了同步

volatile的实现原理 

  有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令。在CPU级别的功能如下:

  • 将当前处理器缓存行的数据写回到系统内存
  • 这个写回内存的操作会告知在其他CPU你们拿到的变量是无效的下一次使用时候要重新共享内存拿

  使用 volatile 必须具备的条件

  •   对变量的写操作不依赖于当前值。
  •   该变量没有包含在具有其他变量的不变式中。只有在状态真正独立于程序内其他内容时才能使用 volatile。

写理解

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

 读理解

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

 

volatile 指令重排

  volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。关于内存屏障的具体讲解以前写过不再重复,JMM装逼于无形这里说过。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:

  •   第一个为读操作时,第二个任何操作不可重排序到第一个前面。
  •   第二个为写操作时,第一个任何操作不可重排序到第二个后面。
  •   第一个为写操作时,第二个的读写操作也不运行重排序。

 内存屏障

   内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。

  编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

   在多线程环境里需要使用某种技术来使程序结果尽快可见。请先假定一个事实:一旦内存数据被推送到缓存,就会有消息协议来确保所有的缓存会对所有的共享数据同步并保持一致。这个使内存数据对CPU核可见的技术被称为内存屏障或内存栅栏

  内存屏障提供了两个功能。

  •   它们通过确保从另一个CPU来看屏障的两边的所有指令都是正确的程序顺序,
  •   而保持程序顺序的外部可见性;其次它们可以实现内存数据可见性,确保内存数据会同步到CPU缓存子系统。

  Java内存模型中volatile变量在写操作之后会插入一个store屏障,在读操作之前会插入一个load屏障。一个类的final字段会在初始化后插入一个store屏障,来确保final字段在构造函数初始化完成并可被使用时可见。

  Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。

Store Barrier

  Store屏障,是x86的”sfence“指令,强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,并把store缓冲区的数据都刷到CPU缓存。这会使得程序状态对其它CPU可见,这样其它CPU可以根据需要介入。

Load Barrier

  Load屏障,是x86上的”ifence“指令,强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行,并且一直等到load缓冲区被该CPU读完才能执行之后的load指令。这使得从其它CPU暴露出来的程序状态对该CPU可见,

Full Barrier

  Full屏障,是x86上的”mfence“指令,复合了load和save屏障的功能。

内存屏障的性能影响

  内存屏障阻碍了CPU采用优化技术来降低内存操作延迟,必须考虑因此带来的性能损失。为了达到最佳性能,最好是把要解决的问题模块化,这样处理器可以按单元执行任务,然后在任务单元的边界放上所有需要的内存屏障。采用这个方法可以让处理器不受限的执行一个任务单元。合理的内存屏障组合还有一个好处是:缓冲区在第一次被刷后开销会减少,因为再填充改缓冲区不需要额外工作了。

volatile的应用场景

状态标志 

   volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;
//thread stop
public void shutdown() { 
    shutdownRequested = true; 
}
//thread work
public void doWork() {
    while (!shutdownRequested) {// do stuff}
}

一次性安全发布

  缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;
    public void initInBackground() {
    // do lots of stufftheFlooble = new Flooble();
     // this is the only write to theFlooble
    }
}
public class SomeOtherClass {
    public void doWork() {
        while (true) {
            // do some stuff...
            // use the Flooble, but only if it is ready
        if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble);
    }
  }
}

  在 VolatileCachedFactorizer使用了OneValueCache来保存缓存的数值及其因数。咱们将 OneValueCache 声明为 volatile,这样当一个线程将cache设置为引用一个新的OneValueCache时,其它线程就会立刻看到新缓存的数据。

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache =
        new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);//声明为 volatile 。防止指令重排序,保证可见性
        }
        encodeIntoResponse(resp, factors);
    }
}

 安全地公布一个对象。对象的应用以及对象的状态必须同一时候对其它线程可见。一个正确构造的对象可以经过下面方式来安全地公布:

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的应用保存到volatile类型的域或者AtomicReferance对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中。

模独立观察(independent observation)

  安全使用 volatile 的另一种简单模式是定期 发布 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

public class UserManager {
  public volatile String lastUser;
  public boolean authenticate(String user, String password) {
    boolean valid = passwordIsValid(user, password);
    if (valid) {
      User u = new User();
      activeUsers.add(u);
      lastUser = user;
    }
    return valid;
  }
}

volatile bean 模式

   在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。

@ThreadSafe
public class Person {
  private volatile String firstName;
  private volatile String lastName;
  private volatile int age;
  public String getFirstName() {
    return firstName;
  }
  public String getLastName() {
    return lastName;
  }
  public int getAge() {
    return age;
  }
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
  public void setAge(int age) {this.age = age;}
}

 

 

posted on 2021-05-13 11:48  书梦一生  阅读(139)  评论(0编辑  收藏  举报

导航