【问题标题】:Necessity of volatile array write while in synchronized block在同步块中写入易失性数组的必要性
【发布时间】:2012-02-08 07:31:30
【问题描述】:

关于 JMM 的问题以及关于在同步块中写入但未同步读取的 volatile 字段的语义。

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

  // 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 将其标记为无效)。对于同步块,我认为它仍然是必要的(因为读取是不同步的),但我只是想确认一下,因为如果实际上不需要它,它在代码中看起来很荒谬。我不确定在同步块结束和易失性读取之间是否有任何我不知道的保证。

【问题讨论】:

    标签: java multithreading thread-safety


    【解决方案1】:

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

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

    【讨论】:

    • 感谢您的提示,我同意在理想情况下,防御性副本将是一件好事,但此处缓存的数组不会被修改,因此性能提升超过不制作副本是可取的(如果这改变了缓存可以由返回防御性副本的实现包装)。我有理由确定编写的代码是正确的,我只是不确定我是否可以删除易失性自分配,因为我已经添加了部分同步(根据我对另一个答案的评论,我相信我仍然需要它,只是想要验证)。
    • +1 作为答案,但我接受了另一个答案,因为它更直接地回答了这个问题。
    【解决方案2】:

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

    换句话说

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

    具有与

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

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

    【讨论】:

    • 我也有同样的感觉,但是由于他后来写入 volatile 数组,并且由于 volatile 具有“级联效应”,所以在写入 volatile 字段之前写入的所有内容,我认为其实没有知名度问题。虽然它非常脆弱,但使用同步的 getData 会更加清晰和健壮。
    • 问题是 this.cache = this.cache 的非易失性存储和易失性存储之间没有发生之前的关系
    • 有一个,因为对缓存[row][col]的非易失性写入是在同一个线程中对缓存的易失性写入之前进行的。易失性写入发生在读取缓存之前。
    • 但是由于缓存实例已经发布,其他线程可以在正常加载后读取缓存的索引并读取一个陈旧的值,直到易失性写入
    • 我明白 volatile 数组元素不能保证,这也是后面自赋值的原因。我的理解是 volatile 自分配将确保其他线程看到更新的缓存值(在自分配之后)。如果他们暂时看到陈旧的价值,我并不担心。我的总体问题是我是否仍然需要同步块的这种自分配,或者同步块的末尾是否与块中读取的易失性字段有任何交互(根据您的回答和我自己的理解,我认为不是,只是想验证)。
    【解决方案3】:

    是的,根据 Java 内存模型,您仍然需要 volatile 写入。 解锁cache没有同步顺序 到cache 的后续易失性读取: unlock -> volatileRead 不保证可见性。 您需要 unlock -> lockvolatileWrite -> volatileRead

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

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

    额外的 volatile 写入的相对开销似乎并没有那么大。

    您的代码正确且高效;但是它超出了典型模式;我会稍微调整一下:

      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; 
          }
        }
      }
    

    【讨论】:

    • 谢谢你的回答,很清楚并证实了我的怀疑。关于拥有两个缓存变量的建议也可能是一个好主意,因为它避免了有人删除自分配(尽管 cmets 警告不要这样做)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-07
    • 2014-10-09
    • 2011-07-07
    • 2016-07-02
    相关资源
    最近更新 更多