转载

记一次JVM指令重排引起的线程问题

多个线程同时读一个HashMap,有一条线程会定时获取最新的数据,构建新的HashMap并将旧的引用指向新的HashMap,这是一种类似CopyOnWrite(写时复制)的写法,主要代码如下,在不要求数据实时更新(容忍数据不是最新),并且各个线程之间容忍看不到对方的最新数据的情况下,这么这种写法安全吗?

public class myClass {
    private HashMap<String,String> cache = null;
    public void init() {
        refreshCache();
    }
    // this method can be called occasionally to update the cache.
    public void refreshCache() {
        HashMap<String,String> newcache = new HashMap<String,String>();
        newcache.put("key", "value"); // code to fill up the new cache
        // and then finally
        cache = newcache; //assign the old cache to the new one in Atomic way
    }
}
复制代码

解答

这种写法并不安全,HashMap需要声明为volatile后才是安全的

延伸

很多资料都会介绍volatile是易失性的,各个线程可以实时看到volatile变量的值, 这种解释虽然没错但是不完整,容易误导开发人员(包括本文遇到的问题) ,同时这种解释没有深入到JVM的happen-before,建议大家少看这种解释

JVM的会对指令进行优化重排,虽然书写顺序是A先于B,但可能执行结果是B先于A,要避免这种问题就要用到happen-before

happen-before有以下八大原则:

  • 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  • 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  • volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
  • happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  • 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  • 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  • 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  • 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

被声明为volatile的变量满足happen-before原则,对此变量的写操作总是happen-before于其他操作,所以才会出现其他文章关于volatile对其他线程可见的解释, 可见是表象,happen-before才是原因

在本文的问题中,如果没有volatile, 不满足happen-before的原则, JVM会对指令进行重排,cache = newcache可能先于newcache.put("key", "value") ,如果此时其他线程读取了HashMap,就会找不到数据,换句话说这种写法是线程不安全的.

原文  https://juejin.im/post/5d4971086fb9a06ad229b610
正文到此结束
Loading...