Java 同步块中易失性数组写入的必要性

Java 同步块中易失性数组写入的必要性,java,multithreading,thread-safety,Java,Multithreading,Thread Safety,关于JMM的问题以及关于在同步块中写入但读取不同步的易失性字段的语义 在下面代码的初始版本中,我没有同步访问,因为它对于早期的需求是不必要的(并且滥用自分配this.cache=this.cache确保了不稳定的写语义)。某些要求已更改,需要同步以确保不会发送重复的更新。我的问题是,同步块是否排除了对易失性字段的自赋值 // Cache of byte[] data by row and column. private volatile byte[][][] cache; publ

关于JMM的问题以及关于在同步块中写入但读取不同步的易失性字段的语义

在下面代码的初始版本中,我没有同步访问,因为它对于早期的需求是不必要的(并且滥用自分配this.cache=this.cache确保了不稳定的写语义)。某些要求已更改,需要同步以确保不会发送重复的更新。我的问题是,同步块是否排除了对易失性字段的自赋值

  // Cache of byte[] data by row and column.
  private volatile byte[][][] cache;

  public byte[] getData(int row, int col)
  {
    return cache[row][col];
  }

  public void updateData(int row, int col, byte[] data)
  {
    synchronized(cache)
    {
      if (!Arrays.equals(data,cache[row][col]))
      {
        cache[row][col] = data;

        // Volatile write.
        // The below line is intentional to ensure a volatile write is
        // made to the array, since access via getData is unsynchronized.
        this.cache = this.cache;

        // Notification code removed
        // (mentioning it since it is the reason for synchronizing).
      }
    }
  }

如果没有同步,我相信自分配volatile写入在技术上是必要的(尽管IDE将其标记为无效)。对于synchronized块,我认为它仍然是必要的(因为读取是不同步的),但我只想确认一下,因为如果它不是实际需要的话,在代码中看起来很可笑。我不确定在同步块的结束和易失性读取之间是否有任何我不知道的保证。

自分配确保另一个线程将读取已设置的数组引用,而不是另一个数组引用。但是您可能有一个线程在修改数组,而另一个线程在读取它


对阵列的读取和写入都应同步。此外,我不会盲目地在缓存中存储和返回数组。数组是可变的、非线程安全的数据结构,任何线程都可能通过改变数组来损坏缓存。应该考虑创建防御性拷贝,和/或返回一个不可修改的列表,而不是字节数组。

< P>一个写入易失性数组的索引实际上没有内存效果。也就是说,如果您已经实例化了数组,那么将该字段声明为volatile将不会为您提供分配给数组中的元素时所需的内存语义

换句话说

private volatile byte[][]cache = ...;
cache[row][col] = data;
具有与相同的内存语义

private final byte[][]cache = ...;
cache[row][col] = data;

因此,必须同步对阵列的所有读写操作。当我说“相同内存语义”时,我的意思是不能保证线程将读取
cache[row][col]

的最新值。是的,根据Java内存模型,您仍然需要易失性写入。 解锁
缓存时没有同步顺序
缓存的后续易失性读取
: 解锁->挥发头不保证可见性。 您需要解锁->锁定或挥发写入->挥发读取

然而,真正的JVM具有更强的内存保证。通常解锁和volatileWrite具有相同的记忆效果(即使它们在不同的变量上);与锁和挥发头相同

所以我们在这里有一个进退两难的局面。典型的建议是你应该严格遵守规范,除非你对这个问题有非常广泛的了解。例如,JDK代码可能会使用一些理论上不正确的技巧;但是代码针对的是一个特定的JVM,作者是专家

额外的易失性写入的相对开销似乎没有那么大

你的代码是正确和高效的;然而,这超出了典型模式的范围;我会稍微调整一下,比如:

  private final    byte[][][] cacheF = new ...;  // dimensions fixed?
  private volatile byte[][][] cacheV = cacheF;

  public byte[] getData(int row, int col)
  {
    return cacheV[row][col];
  }

  public void updateData(int row, int col, byte[] data)
  {
    synchronized(cacheF)
    {
      if (!Arrays.equals(data,cacheF[row][col]))
      {
        cacheF[row][col] = data;

        cacheV = cacheF; 
      }
    }
  }

我也有同样的感觉,但由于他后来写入volatile数组,而且volatile具有“级联效应”,在写入volatile字段之前写入的所有内容上,我认为实际上不存在可见性问题。但是它非常脆弱,使用同步的getData会更加清晰和健壮。但是问题是,this.cache=this.cacheThere的非易失性存储和易失性存储之间没有发生之前的关系,因为非易失性写入缓存[row][col]是在易失性写入缓存之前进行的,在同一条线上。并且易失性写入发生在缓存读取之前。但是,由于缓存实例已经发布,其他线程可以在正常加载后读取缓存的索引,并读取过时的值,直到易失性写入知道不保证使用易失性数组元素,这就是以后进行自分配的原因。我的理解是,volatile自分配将确保其他线程看到更新的缓存值(在自分配之后)。如果他们只是暂时看到过时的价值,我并不担心。我的总体问题是,我是否仍然需要同步块的自赋值,或者同步块的末尾是否与块中读取的易失性字段有任何交互(根据您的回答和我自己的理解,我认为不是,只是想验证)。感谢您提供的提示,我同意在理想世界中,制作防御性拷贝是一件好事,但此处缓存的阵列不会被修改,因此与不制作拷贝相比,性能提升更为可取(如果这一点发生变化,缓存可以由返回防御性拷贝的实现包装)。我相当确信所编写的代码是正确的,我只是不确定我是否可以删除volatile self赋值,因为我已经添加了部分同步(根据我对另一个答案的评论,我相信我仍然需要它,只是想验证一下)。+1对于答案,但我接受了另一个答案,因为它更直接地回答了这个问题。谢谢你的回答,它很清楚,证实了我的怀疑。关于拥有两个缓存变量的建议也可能是一个好主意,因为它避免了有人删除自分配(尽管注释警告不要这样做)。