783°

Java 多线程经典面试题volatile

问题

当多个线程并发同时进行set、get时,其它线程能否感知到flag的变化

public class ThreadSafeCache {

    boolean flag = true;//默认设置true

    public boolean isFlag() {
        return flag;
    }

    public synchronized ThreadSafeCache setFlag(boolean flag) {
        this.flag = flag;
        return this;
    }

    public static void main(String[] args) {
        ThreadSafeCache threadSafeCache = new ThreadSafeCache();
        //循环创建多个线程
        for (int i = 0;i < 10;i++){
            new Thread(() -> {
              int j = 0;
              while(threadSafeCache.isFlag()){
                  j++;
              }
              System.out.println(j);
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadSafeCache.setFlag(false);
    }
}

运行结果 未添加volatile运行结果 可以看到程序是卡死了,一直没有退出

分析

这个类非常简单,里面有一个属性,有两个方法,set、get,并且在set方法上添加了synchronized。

多线程并发的同时进行set、get操作,A线程调用set、B线程调用get能感知到flag发生变化吗?

说到这里,问题就变成了synchronized能否保证上下文可见性!!!

关键词synchronized的用法

  • 指定加锁对象:对给定的对象进行加锁,进入同步代码前需要获得给定对象的锁。
  • 直接作用于实例方法:相当于对当前对象的实例加锁,进入同步代码前需要获得当前对象实例的锁
  • 直接作用于静态方法:相当于对当前类进行加锁,进入同步代码前需要获得当前类的锁。

从代码中,我们可以看到只对set方法加了同步锁,多个线程调用set方法时,由于存在锁,会一个一个的进行set,但对于get来说,并没有加锁,多个线程无需获得该实例的锁,就可以直接获取到flag的值,那么我们就需要考虑某一个线程set之后的flag对其它线程是否可见!!!

Java内存模型happens-before原则

JSR-133内存模型使用happens-before原则的概念来阐述操作之间的内存可见性。在JMM(JAVA Memory Model)中,如果一个执行的结果需要对另一个操作可见,那么这两个操作直接必须要存在happens-before关系。两个操作可以是同一个线程内的也可以是不同线程中的。

happens-before(之前发生)原则

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器的解锁,happens-before于随后对这个监视器的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • 线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值的手段检测到线程是否已经终止执行。
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

注意:两个操作之间存在happens-before关系,并不一定前一个操作必须要在后一个操作执行!!!

happens-before仅仅要求前一个操作的执行结果对后一个操作可见,且前一个操作的执行顺序排在后一个操作之前(因为java虚拟机重排不相关的指令)。

volatile

volatile可见性

前面的happens-before原则中提到了volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。因此,volatile保证了多线程下的可见性!!!

volatile禁止内存重排序

下面是JMM针对编译器制定的volatile重排序规则:

是否能重排序 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写 NO
volatile读 NO NO NO
volatile写 NO NO

通过上面的分析我们添加关键字volatile来试试 添加volatile运行结果

结论

多线程并发的同时进行set、get操作,A线程调用set方法,B线程并不一定能对这个改变可见,上面的代码中,如果get也添加synchronized也是可见的,还是happens-before的监视器规则:对一个监视器的解锁,happens-before于随后对这个监视器的加锁。只是volatile对比synchronized更轻量级,所以本例使用volatile,但是对于符合非原子操作i++这里还是不行的,还得用synchronized。

不过使用volatile也会限制一些调优

关注我获取更多咨询


全部评论: 0

    我有话说: