【问题标题】:Using volatile collections and arrays in Java在 Java 中使用 volatile 集合和数组
【发布时间】:2014-08-29 19:13:20
【问题描述】:

想象一下我们有

volatile int publisher = 0;
volatile List<String> list = Arrays.asList("Buenos Aires", "Córdoba", "La Plata");
volatile String[] array = {"Buenos Aires", "Córdoba", "La Plata"};

据我所知。

列表和数组中的初始值已正确发布,并且对所有读取线程可见。

初始化后添加的所有值都不是安全发布的。

我们仍然可以使用

安全地阅读和发布它们
//in Thread 1
list.add("Safe City");
array[2] = "Safe city";
publisher = 1;

//in Thread2

if(publisher == 1) {
String city = list.get(3);
city = array[2];
}

我说的对吗?

【问题讨论】:

  • 请注意,volatile 关键字适用于列表或数组变量本身,而不适用于列表或数组的内容。
  • 不清楚你在这里问什么。对于原语,volatile 指的是原语值本身。对于对象,volatile 关键字仅指引用,而不是引用的对象。所以对于listarrayvolatile 对列表或数组的内容没有任何影响。 volatile 并没有像你想象的那样做。
  • @JimGarrison 在 Java 1.5 和更新版本中,volatile 保证了在分配给 volatile 之前所做的所有更改的可见性,而不仅仅是 vloatile 本身的更改。所以它可能是相关的。
  • 请注意,您不能在Arrays.asList返回的List中添加或删除元素;您只能设置元素而不更改列表的大小。不过,这与您询问的内存模型问题几乎无关。

标签: java collections concurrency volatile java-memory-model


【解决方案1】:

只看代码在做什么,仅凭内存模型评估它,你是对的。线程 1 中对 volatile 变量 publisher 的写入和线程 2 中对 volatile 变量的读取建立了 happens-before 关系,因此线程 1 中的所有先前写入将对后续从线程 1 中的读取可见线程 2.

正如 CupawnTae 所指出的,列表和数组没有必要为 volatile 以使其保持不变。只有publisher 需要是可变的。

从更广泛的角度来看,很难扩展这段代码来做其他事情。 (撇开Arrays.asList 返回的List 不能添加元素这一事实不谈;假设它是ArrayList。)大概线程1 或其他线程将希望继续将元素添加到列表中.如果这碰巧导致ArrayList 重新分配其底层数组,这可能发生在线程 2 仍在读取先前添加的结果时。因此,线程 2 可能会看到不一致的状态。

进一步假设线程 1 想要进行后续更新。它必须将publisher 设置为其他值,比如2。现在阅读线程如何知道要测试的正确值是什么?好吧,他们可以从其他一些 volatile 变量中读取预期值....

毫无疑问,可以构建一个线程 1 可以随意写入列表(或数组)的方案,而线程 2 除了一致的快照之外什么都看不到,但是您必须在每一步都特别注意内存可见性道路。在某些时候,使用锁更容易。

【讨论】:

    【解决方案2】:

    没错,但是……

    列表和数组上的 volatile 关键字在这里无关紧要 - 事实上,您将值写入 volatile publisher 您写入其他值之后,然后读回if 条件中的值读取第二个线程中的其他值可确保这些线程之间的内存一致性。

    如果您从列表和数组中删除 volatile 关键字,您的代码仍然是安全的。

    如果删除publisher 变量写入/读取,则add 操作* 和数组赋值不再安全。

    是的,对变量的初始赋值也是安全的。

    * 正如 Stuart Marks 所指出的那样,这实际上在该特定列表上实际上是无效的,但让我们假设它是例如一个数组列表

    【讨论】:

    • 感谢 CupawnTae,但如果我根本没有“发布者” - 我是否正确:“列表和数组中的初始值已正确发布并且对所有阅读线程可见。所有值初始化后添加的不是安全发布的”?
    • @Dymytry 请查看更新后的答案 - 我已经详细介绍了,应该澄清
    • “如果你对 volatile 变量添加一个写操作,即使值没有改变,它也会再次变得安全”——这是错误的。如果读取线程无法检测到写入是否发生(如果值相同则不可能),则不存在 happens-before 关系。对volatile 变量的写入不会导致读取该变量的其他线程等待写入发生。问题中的代码是正确的,因为publisher01 的转换是可检测到的并且由阅读线程检查。没有它,它就会被破坏。
    • @Holger 有趣的点。我想我对“安全”的定义在这里有点松散。虽然读取线程在读取变量时将得到一致的图片,但在此之后没有什么可以阻止另一个线程修改集合。但是,publisher 检查也不会阻止这种情况的发生,不是吗?编写器进行更改,更新publisher,读取器看到更改,读取变量,编写器进行更多更改,读取器读取不一致的数据?无论哪种方式,我都删除了该部分,因为它即使不是完全错误也具有误导性。谢谢。
    • @CupawnTae:是的,任何后续修改都可能导致数据竞争,随后对volatile 变量的写入也无济于事。使用volatile 变量进行保护是一次性操作。这就是为什么Stuart Marks said “it's very difficult to extend this code to do anything else”
    【解决方案3】:

    “发布”发生在设置 volatile 值的线程和获取它的线程之间。

    两个都需要

    publisher = 1;
    

    在一个线程中和

    int local = publisher;
    

    在另一个。

    【讨论】:

    • 问题中已经存在的if (publisher==1) 足以满足第二个约束
    【解决方案4】:

    您是否考虑过使用synchronized 块来锁定您尝试读/写的数据结构?

    //in Thread 1
    synchronized(someLockingMonitor) {
        list.add("Safe City");
        array[2] = "Safe city";
    }
    
    //in Thread2
    synchronized(someLockingMonitor) {
        String city = list.get(3);
        city = array[2];
    }
    

    然而,这将强制任何希望访问其中一个块的线程等待,直到当前在这些块之一内执行的任何其他线程离开该块。

    如果并发对您很重要,即您确实希望不同的线程同时读取和写入,请查看java.util.concurrent 中的并发集合。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多