说,我有一个数据对象:
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);
}
}
使用情况:
for (ValueRef x: masterList) { x.value = Math.random(); }
使用某些作业填充作业队列。
唤醒线程池
等到每个作业都被评估
注意:在作业评估期间,所有值都是常量。但是,线程可能已经过去评估了作业,并保留了缓存的值。
问题:确保每个线程看到最新值所需的最小同步量是多少?
我理解从monitor / lock-perspective进行同步,我不明白从cache / flush-perspective进行同步(即在同步块的进入/退出时内存模型保证了什么)。
对我来说,感觉我需要在更新值的线程中同步一次以将新值提交到主内存,并且每个工作线程一次,以刷新缓存以便读取新值。但我不确定如何最好地做到这一点。
我的方法:创建全局监控:static Object guard = new Object();
然后,在guard
上同步,同时更新主列表。最后,在启动线程池之前,对池中的每个线程执行一次,在空块中同步guard
。
这是否真的导致该线程读取的任何值的完全刷新?或者只是在同步块内触及的值?在这种情况下,可能我应该在循环中读取每个值一次而不是空块?
感谢您的时间。
编辑:我认为我的问题归结为,一旦我退出同步块,每次第一次读取(在该点之后)都会进入主存储器吗?无论我同步什么?
答案 0 :(得分:3)
线程池的线程在过去评估过某些作业并不重要。
Executor
的Javadoc说:
内存一致性效果:在将Runnable对象提交给Executor之前,线程中的操作发生在执行开始之前,可能是在另一个线程中。
因此,只要您在提交作业之前使用标准线程池实现并更改数据,就不必担心内存可见性效果。
答案 1 :(得分:2)
你的计划听起来足够了。这取决于你打算如何“唤醒线程池”。
Java内存模型规定,在进入synchronized
块之前由线程执行的所有写操作对于随后在该锁上同步的线程是可见的。
因此,如果您确定在更新主列表期间wait()
调用(必须在synchronized
块内)阻止了工作线程,那么当它们唤醒并成为runnable,主线程所做的修改对这些线程是可见的。
但是,我鼓励您在java.util.concurrent
包中应用更高级别的并发实用程序。这些将比您自己的解决方案更强大,并且是在深入研究之前学习并发的好地方。
只是为了澄清:在不使用同步块的情况下控制工作线程几乎是不可能的,其中进行检查以查看工作者是否有任务要执行。因此,在工作线程唤醒之前,控制器线程对作业所做的任何更改都会发生。您需要synchronized
块或至少volatile
变量作为内存屏障;但是,我无法想象你如何使用其中一个来创建一个线程池。
作为使用java.util.concurrency
软件包的优势示例,请考虑以下事项:您可以使用synchronized
块,其中包含wait()
个调用,或带有volatile
变量的忙等待循环。由于线程之间上下文切换的开销,繁忙的等待在某些条件下实际上可以表现得更好 - 没有必要乍看之下可能会有一个可怕的想法。
如果您使用并发实用程序(在这种情况下,可能是ExecutorService
),您可以根据环境,任务的性质和需求为您的特定案例选择最佳选择。在给定时间的其他线程。自己实现这种优化水平是很多不必要的工作。
答案 2 :(得分:1)
为什么不在发布对集合的引用后,使Collection<ValueRef>
和ValueRef
不可变,或者至少不要修改集合中的值。那你就不用担心同步了。
当您想要更改集合的值,创建新集合并在其中添加新值时。设置值后,通过集合引用新的作业对象。
不这样做的唯一原因是,如果集合的大小太大以至于它几乎不适合内存并且您无法承担两份副本,或者集合的交换会导致垃圾工作量太大collector(在为线程代码使用可变数据结构之前证明其中一个是一个问题)。