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

时间:2012-01-11 22:43:50

标签: java multithreading thread-safety

有关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).
      }
    }
  }

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

3 个答案:

答案 0 :(得分:4)

是的,根据Java Memory Model,你仍然需要volatile写。 解锁cache没有同步顺序 随后对cache的易失性读取: unlock - > volatileRead 不保证可见性。 您需要 unlock - >锁定 volatileWrite - > volatileRead

但是,真正的JVM具有更强大的内存保证。通常 unlock volatileWrite 具有相同的记忆效果(即使它们在不同的变量上);与 lock volatileRead 相同。

所以我们在这里陷入两难境地。典型的建议是你应该严格遵守规范。除非你对此事有广泛的了解。例如,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; 
      }
    }
  }

答案 1 :(得分:3)

自我赋值确保另一个线程将读取已设置的数组引用,而不是另一个数组引用。但是你可能有一个线程修改数组而另一个线程读取它。

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

答案 2 :(得分:3)

对易失性数组的索引写入实际上没有记忆效应。也就是说,如果您已经实例化了数组,那么将字段声明为volatile将不会为您提供在分配给数组中的元素时所寻找的内存语义。

换句话说

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

具有与

相同的内存语义
private final byte[][]cache = ...;
cache[row][col] = data;

因此,您必须同步对阵列的所有读取和写入。当我说'相同的内存语义'时,我的意思是不能保证线程会读取最新值cache[row][col]