【问题标题】:Making sure a thread's "updates" is readable to other threads in Java确保线程的“更新”对 Java 中的其他线程是可读的
【发布时间】:2015-03-05 02:14:22
【问题描述】:

我有一个将启动其他线程的主线程。那些其他线程将要求完成作业,而主线程将使作业可供其他线程查看和执行。

必须完成的工作是将一个巨大的布尔数组中的索引设置为真。它们默认为假,其他线程只能将它们设置为真,从不假。各种作业可能涉及将相同的索引设置为 true。

主线程根据两件事找到新工作。

  1. 巨大的布尔数组中的值。
  2. 哪些工作已经完成。

如何确保主线程从庞大的布尔数组中读取新值?

我不能通过同步方法更新数组,因为这几乎是所有其他线程所做的,因此我只能获得相当多的顺序性能。

假设其他线程通过非同步函数将许多索引设置为 true 来更新巨大的布尔数组。如何确保主线程读取更新并确保它不只是本地缓存在线程中?有没有办法让它“推送”更新?我猜主线程应该只使用同步方法来“获取”更新?

【问题讨论】:

    标签: java multithreading caching


    【解决方案1】:

    要真正完整地回答您的问题,您应该打开 Java 语言规范的副本,然后搜索“发生在之前”。

    当 JLS 说 A“发生在”B 之前,这意味着在 Java 语言的有效实现中,A 必须实际发生在 B 之前。规范中这样说:

    • 如果某个线程更新了一个字段,然后释放了一个锁(例如, 留下一个同步块),更新“发生在”锁定之前 发布,

    • 如果某个线程释放了锁,而其他线程随后释放了锁
      获取相同的锁,释放“发生在”获取之前。

    • 如果某个线程获得一个锁,然后读取一个字段,则 获取“发生在”阅读之前。

    由于“发生在之前”是一种传递关系,你可以推断如果线程 A 更新了同步块中的一些变量,然后线程 B 检查了在同一个对象上同步的块中的变量,那么线程 B 会看到线程 A 写了什么。

    除了进入和离开同步块之外,还有许多其他事件(构造对象、wait()ing/notify()ing 对象、start()ing 和 join()ing 线程、读取和写入 volatile 变量)允许您可以在线程之间建立“发生在之前”的关系。

    这不是解决问题的快速方法,但这一章值得一读。


    ...主线程将使作业可供其他线程查看和执行...

    我不能通过同步方法更新数组,因为几乎所有其他线程都这样做,而且...

    听起来您是在说每个工作线程在必须等待来自 main() 线程的进一步指令之前只能做少量的工作。如果这是真的,那么大多数工人大部分时间都在等待。如果您只在一个线程中完成所有工作,您可能会获得更好的性能。

    假设您的目标是充分利用多处理器机器的可用周期,您将需要以某种方式对工作进行分区,让每个工作线程在需要同步之前完成大部分工作与任何其他线程。

    【讨论】:

    • 好吧,我想拆分成不同线程的工作是将数组中的索引设置为 true。因此,同步设置值的方法会有点愚蠢,因为那样它几乎是连续的。你说等待允许“发生在之前”? :) 我可以使用 CyclicBarrier 吗?
    • 在 CyclicBarrier 的文档中它说,“内存一致性效果:线程中的动作在调用 await() 之前发生的动作是屏障动作的一部分,而这些动作又发生在动作之前在其他线程中从相应的 await() 成功返回之后。”。这是什么意思?
    • @HorseSMith,假设当您说“...同步会有点愚蠢...”时,您的意思是您认为不需要互斥。不幸的是,他们选择了关键字synchronized 进行互斥,因为他们使用JLS 中的短语“同步于”来描述任何参与跨线程“发生之前”情况的事件。 (例如,“对volatile 变量v 的写入任何线程对v 的所有后续读取同步。”)IIRC,他们在记录JLS 内存之前选择了synchronized模型。
    • @HorseSMith Re, CyclicBarrier:如果 A 和 B 都发生在同一个线程中,并且 A 在 程序顺序 中出现在 B 之前,则 A “发生在”B 之前。因此,如果一个线程更新一个字段,然后它调用barrier.await(),然后其他线程随后从barrier.await() 返回,然后读取相同的字段,则另一个线程将保证获得第一个线程写入的值。
    • wait 和 notify/notifyall 也一样吗?
    【解决方案2】:

    我会使用另一种设计模式。例如,您可以在打开布尔值时将它们的索引添加到 Set 中,然后同步对其的访问。然后你可以使用等待/通知唤醒。

    【讨论】:

    • 我已经指出,这将是多余的,它也可能是顺序的,它可能会表现得更差。
    • 我认为您需要提供更多信息。
    • 真的没什么好说的了。每个工作都是将巨大数组中的各种索引设置为true。它如何决定索引?这是一个 for 循环,它在作业告诉它的地方开始和结束,并且步骤与作业所说的一样大。
    【解决方案3】:

    首先,一般不要使用布尔数组,使用BitSets。看到这个:boolean[] vs. BitSet: Which is more efficient?

    在这种情况下,您需要一个原子 BitSet,因此您不能使用 java.util.BitSet,但这里有一个:AtomicBitSet implementation for java

    【讨论】:

    • 虽然我完全同意我应该使用 AtomicBitSet 来代替,但如果我必须使用布尔数组来解决这个问题,有什么好的方法呢?
    【解决方案4】:

    您可以将其建模为消息传递而不是改变共享状态。在您的描述中,工作人员从不读取布尔数组,而只写入完成状态。您是否考虑过使用工作人员从中消费的待处理作业队列和主设备读取的完成队列?主线程可以有效地维护作业状态字段,而无需任何共享状态问题。根据您的需要,您可以使用阻塞或非阻塞队列。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-20
      • 1970-01-01
      • 2014-09-08
      • 2012-08-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多