【问题标题】:Thread Pool, Shared Data, Java Synchronization线程池、共享数据、Java 同步
【发布时间】:2026-01-20 11:25:01
【问题描述】:

说,我有一个数据对象:

class ValueRef { double value; }

每个数据对象存储在主集合中的位置:

Collection<ValueRef> masterList = ...;

我还有一个作业集合,其中每个作业都有一个本地数据对象集合(其中每个数据对象也出现在masterList):

class Job implements Runnable { 
     Collection<ValueRef> neededValues = ...; 
     void run() {
         double sum = 0;
         for (ValueRef x: neededValues) sum += x;
         System.out.println(sum);
     } 
}

用例:

  1. for (ValueRef x: masterList) { x.value = Math.random(); }

  2. 用一些作业填充作业队列。

  3. 唤醒线程池

  4. 等到每个作业都评估完毕

注意:在工作评估期间,所有的值都是不变的。然而,线程可能在过去评估过作业,并保留缓存值。

问题:确保每个线程看到最新值所需的最小同步量是多少?

我理解从监视器/锁定角度同步,我不理解从缓存/刷新角度同步(即,在同步块的进入/退出时内存模型保证了什么)。

对我来说,感觉就像我需要在更新值的线程中同步一次以将新值提交到主内存,并在每个工作线程中同步一次,以刷新缓存以便读取新值。但我不确定如何最好地做到这一点。

我的做法:创建一个全局监控器:static Object guard = new Object(); 然后,在guard 上同步,同时更新主列表。最后,在启动线程池之前,对于池中的每个线程,在一个空块中同步guard

这真的会导致该线程读取的任何值被完全刷新吗?或者只是在同步块内触及的值?在这种情况下,也许我应该循环读取每个值一次,而不是一个空块?

感谢您的宝贵时间。


编辑:我认为我的问题归结为,一旦我退出同步块,每次第一次读取(在那之后)都会进入主内存吗?不管我同步的是什么?

【问题讨论】:

  • 似乎是利用 volatile 关键字的完美场所
  • 我只写了一次(实际上是恒定的),但可能会读数百万次。 Volatile 永远不会在本地缓存。如果我每次都创建线程池,那么代码可以在没有同步/易失性的情况下正常工作(因为不存在先前的缓存)。
  • 我认为这里不需要 volatile。如果 ValueRef 实际上是不可变的,只需使其实际上是不可变的。使用双。在计划之前为每个作业创建一个新集合,并将其包装在 unmodifiableCollection 中(仅作为提醒)。你预见到什么问题?
  • 在作业完成之前是不可变的。然后再次更改值并重新启动作业。

标签: java threadpool synchronize


【解决方案1】:

线程池中的线程过去评估过一些作业并不重要。

Executor 的 Javadoc 说:

内存一致性效果:线程中的操作在将 Runnable 对象提交给 Executor 之前发生在其执行开始之前,可能在另一个线程中。

因此,只要您使用标准线程池实现并在提交作业之前更改数据,您就不必担心内存可见性影响。

【讨论】:

  • 这是因为……在工作线程中,有一个同步块在等待新的作业?当该块退出时,线程整个缓存被清除了吗?我可以随机同步一些东西并获得相同的效果吗?
  • @AndrewRaffensperger:如何实现并不重要——有保证,应该提供。关于最后一个问题 - 基本上是这样,但没有任何意义:如果没有额外的同步方式,你不能说工作线程中的同步块在主线程中的同步块之后执行;使用额外的同步方式是多余的。
【解决方案2】:

您的计划听起来已经足够了。这取决于您计划如何“唤醒线程池”。

Java 内存模型规定,线程在进入 synchronized 块之前执行的所有写入对于随后在该锁上同步的线程都是可见的。

因此,如果您确定在更新主列表期间工作线程在 wait() 调用(必须在 synchronized 块内)中被阻塞,当它们唤醒并变为可运行时,修改这些线程可以看到主线程生成的内容。

不过,我鼓励您在 java.util.concurrent 包中应用更高级别的并发实用程序。这些将比您自己的解决方案更强大,并且是在深入研究之前学习并发性的好地方。


澄清一下:如果不使用同步块来检查工作线程是否有要执行的任务,几乎不可能控制工作线程。因此,控制器线程对作业所做的任何更改都发生在工作线程唤醒之前。您需要一个synchronized 块,或至少一个volatile 变量来充当内存屏障;但是,我想不出您将如何使用其中之一来创建线程池。

作为使用java.util.concurrency 包的优势的示例,请考虑以下情况:您可以使用带有wait() 调用的synchronized 块,忙等待循环带有volatile 变量。由于线程之间上下文切换的开销,繁忙的等待实际上可以在某些条件下执行得更好——这并不一定是乍一看可能会假设的可怕想法。

如果您使用并发实用程序(在这种情况下,可能是 ExecutorService),可以为您做出适合您特定情况的最佳选择,同时考虑环境、任务的性质以及其他人的需求给定时间的线程。自己实现该级别的优化是很多不必要的工作。

【讨论】:

  • 我负担不起 java.util.concurrent 的开销。我的示例中的数据更新一次,然后在多线程评估期间变为“常量”。我对这些数据如何对其他预先存在的线程可见感兴趣。似乎任何同步块,即使没有任何同步发生之前的关系,都会导致这种可见性。或者可能发生之前不需要任何显式同步,并且“在进行所有值更改之前不会运行任何作业”符合要求。
  • @AndrewRaffensperger 对。如果这就是您所需要的,那么有一个java.util.concurrent 实用程序具有正确性所需的最小开销。假设并发实用程序具有更高的开销是错误的。事实上,它们提供了对高性能并发工具的访问,例如比较和交换。在 Java 中自己实现这一点将比 AtomicXXX 类背后的优化本机代码慢。大多数其他实用程序都具有类似的性能优势。
【解决方案3】:

为什么不让Collection&lt;ValueRef&gt;ValueRef 不可变,或者在发布对集合的引用后至少不要修改集合中的值。这样你就不用担心同步了。

也就是说,当您想要更改集合的值时,创建一个新集合并将新值放入其中。一旦设置了值,就会传递集合引用新的作业对象。

不这样做的唯一原因是如果集合的大小太大以至于它几乎无法放入内存并且您不能拥有两个副本,或者集合的交换会导致垃圾的工作量过多收集器(在为线程代码使用可变数据结构之前证明其中之一是一个问题)。

【讨论】:

  • 对,我总是可以重建 ValueRef 或重建线程池,我的问题就消失了。但在我的实际实现中,数据结构非常复杂,而且代码调用的频率足够高,每次求值时重建线程池开销太大。