【问题标题】:Java: serial thread confinement questionJava:串行线程限制问题
【发布时间】:2011-02-17 05:40:54
【问题描述】:

假设您有一个具有可变状态的 Runnable 的 Collection(ConcurrentLinkedQueue)。线程 A 遍历 Collection 并将 Runnables 交给 ExecutorService。 run() 方法更改 Runnables 状态。 Runnable 没有内部同步。

以上是重复动作,工作线程需要查看之前迭代所做的更改。所以一个 Runnable 被一个接一个的工作线程处理,但一次永远不会被多个线程访问 -> 串行线程限制的情况(我希望 ;))。

问题:它是否仅适用于 ConcurrentLinkedQueue/ExecutorSerivce 的内部同步?

更准确地说:如果线程 A 将 Runnable R 交给工作线程 B 并且 B 更改了 R 的状态,然后 A 将 R 交给工作线程 C..C 是否看到 B 所做的修改?

编辑: 也因为答案完全不同,这个问题让我很忙……来自 JCIP,16.2.2 安全出版,p。 346:

[...] 如果线程 A 将 X 放在 BlockingQueue 上(并且随后没有线程修改它)并且线程 B 从队列中检索它,则 B 可以保证在 A 离开时看到 X。这是因为 BlockingQueue 实现有足够的内部同步来确保 put 发生在 take 之前。[...]

因此,由于 ExecutorService 的实现方式,唯一的保证是工作线程始终将 Runnables 视为 提交 线程离开它们。

回到我的场景。首先 A 通过 ExecutorService 将 R 交给 B,一切都很好,B 看到了最新的 R(“BlockingQueue 保证”)。现在 B 修改了 R,然后 A 将其交给 C。所以需要的是 B 留下的 R。但是我们得到的是R作为A离开了它。

在 B 中所做的更改不能保证在 A 中可见,即使 B 在 A 将其交给 ExecutorService 之前完成了 R 的执行。 在运行 R 之后,A 和 B 之间没有同步。也许 B 将一个变量加载到某种本地缓存中并在那里更新它。

那么如果A在B执行后看不到R的当前状态,C怎么能看到呢?缺少的是从工作线程返回到 A 的安全发布。

如果我错了,这意味着 B 所做的修改对 A 是可见的,尽管 B 除了拍摄之外不进行同步。这种保证隐藏在哪里?

【问题讨论】:

  • 我认为它不能那样工作..但是问题是:除了使 Runnable 线程安全之外,我怎样才能实现我想要的?有没有机制可以让工作线程修改对象后的整个状态可见?
  • 你为什么不想让runnable同步?这似乎是您想要的 - 提供序列化执行并使更改对其他线程可见。它还明确了您正在做什么以及您期望的行为,这比依赖某些组件(例如执行程序)的内部或细节更可靠和可维护。
  • 我们为什么不让所有东西都同步呢? - 性能问题。

标签: java concurrency synchronization


【解决方案1】:

所以如果我理解正确的话,队列本身并没有改变(没有添加或删除元素),只修改了它的元素,一次最多修改一个线程。而且这些元素(Runnables)不是线程安全的。

我认为您可能仍然会遇到不同线程之间更改可见性的问题。如果线程 A 在 Runnable R 中引起了更改,则无法保证下一个线程 B(或任何其他线程,就此而言!)将看到线程 A 所做的更改,除非 R 本身是线程-安全。

更准确地说,如果修改了字段R.f,则只有当f被声明为volatile,或者只能通过@987654326访问时,才能保证f的修改值对其他线程可见@ 块(或者如果它被声明为final,但显然你不能改变它的值 - 如果f 是一个引用,那么只有被引用对象的状态。在这种情况下,问题变成了是否被引用的对象本身是线程安全的)。

更新:您在评论中提问:

除了使 Runnable 线程安全之外,我怎样才能实现我想要的?

您想要的实际上是让您的 Runnable 线程在可见性方面是安全的。所以你的问题在术语上几乎是矛盾的。引自Java Concurrency in Practice,第 3.1.3 节。锁定和可见性:

内在锁定可用于保证一个线程以可预测的方式看到另一个线程的影响 [...]。当线程 A 执行了一个synchronized 块,随后线程 B 进入一个由同一个锁保护的synchronized 块时,在释放锁之前对 A 可见的变量的值保证在获得锁后对 B 可见锁。换句话说,当 A 执行由同一锁保护的 synchronized 块时,A 在 synchronized 块中或之前所做的所有事情对 B 都是可见的。 没有同步,就没有这样的保证。

从第 3.1.4 节开始。可变变量:

volatile 变量的可见性影响超出了 volatile 变量本身的值。当线程 A 写入 volatile 变量并且随后线程 B 读取相同的变量时,在写入 volatile 变量之前对 A 可见的所有变量的值在读取 volatile 变量后对 B 可见。因此,从内存可见性的角度来看,写入 volatile 变量就像退出 synchronized 块,读取 volatile 变量就像进入 synchronized 块。但是,我们不建议过于依赖 volatile 变量来获得可见性;依赖 volatile 变量来查看任意状态的代码比使用锁定的代码更脆弱,更难理解。

仅当volatile 变量简化了同步策略的实施和验证时才使用它们; 在验证正确性时避免使用volatile 变量需要对可见性进行微妙的推理volatile 变量的良好用途包括确保它们自己的状态、它们所引用的对象的可见性,或指示发生了重要的生命周期事件(例如初始化或关闭)。

所有这一切的底线是:如果你希望你的类是线程安全的,最好让它成为线程安全的 :-) 请注意,即使你不能修改原始 Runnable 类的代码,你仍然可以围绕它创建一个线程安全的包装器并通过包装器发布它,从而有效地使其使用线程安全。

但是,如果(出于某种我不知道的原因)您不希望或无法使其完全线程安全,您可以(自担风险)尝试使用上述规则:如果可以组织您的代码,使 Runnable R 的字段的更新顺序在所有线程中始终相同,您可以尝试声明最后修改的字段 volatile(或其访问器 synchronized) ;这将在理论上保证所有对其他字段的其他修改与最后一个字段的更新一起对其他线程可见。对我来说,这种诡计显然属于应该避免的类别——根据上面粗体引用的建议。

【讨论】:

    【解决方案2】:

    你需要问自己是否有同步点。这可能发生在易失性写入、线程启动、内在锁定和 j.u.c.Lock 锁定上。在所有这些情况下,所有其他线程都可以看到任何更新的字段。

    考虑到没有一个字段是易失的或正确同步的,同时考虑到执行器服务重新使用线程(所以没有调用 start()),我假设更新的字段可能不会被所有线程看到所有场合。

    但是正确同步的是执行器服务。该服务使用一个 BlockingQueue 来进行 j.u.c.Lock 锁定。因此,当一个可运行对象被执行时,当该可运行对象从该工作队列中添加和删除时,会有一个同步点。

    【讨论】:

    • 所以在提交Runnable到ExecutorService的时候有一个同步点。这不意味着我描述的场景有效吗? A 提交 R -> 同步点 -> R 被执行 -> A 提交 R -> 同步点 -> R 被执行 -> ...?还是我弄错了?
    • 每个runnable提交的时候都有一个同步点,你是对的。重要的是要知道为什么会有。例如,Java 7 中的 ExecutorService 从 BlockingQueue 切换到 TransferQueue,然后没有保证这些字段将可见,因为 TransferQueue 是非阻塞的。
    【解决方案3】:

    假设在您的实现中,A 需要知道 B 已使用 R 完成,然后再将 R 交给 C。

    从 ExecutorService 的 javadoc 中,该实现已正确同步。在 B 中所做的更改在 C 中可见。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-02-17
      • 2019-03-15
      • 1970-01-01
      • 2017-11-15
      • 1970-01-01
      • 2020-03-04
      • 2016-06-23
      相关资源
      最近更新 更多