Java:串行线程限制问题

时间:2010-05-08 20:10:47

标签: java concurrency synchronization

假设您有一个具有可变状态的Runnables的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保证将X视为A离开它。这是因为BlockingQueue实现具有足够的内部同步,以确保在采取之前发生put。[...]

因此,由于ExecutorService的实现方式,所给出的唯一保证是工作线程总是看到Runnables,因为提交线程留下了它们。

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

在A中完成的更改不保证在A中可见,即使B在将A交给ExecutorService之前完成R的操作也是如此。运行后,A和B 之间没有同步。也许B将变量加载到某种本地缓存并在那里更新。

所以如果A在B执行后没有看到R的当前状态,C怎么办?缺少的是工作者线程安全发布回A。

如果我错了,那就意味着B所做的修改对于A来说是可见的,除了拍摄之外,B不进行同步。隐藏保证的地方?

3 个答案:

答案 0 :(得分:2)

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

考虑到没有任何字段是volatile或正确同步的,还考虑到执行程序服务重新使用线程(因此没有调用start()),我会假设更新的字段可能不会被所有线程看到所有场合。

但正确同步的是执行者服务。该服务使用阻塞j.u.c.Lock的BlockingQueue。因此,当执行一个runnable时,在添加runnable并从该worker队列中删除时会有一个同步点。

答案 1 :(得分:1)

因此,如果我理解正确,队列本身不会更改(没有添加或删除元素),只会修改其元素,最多只能修改一个线程。而这些元素(Runnables)不是线程安全的。

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

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

更新:您在评论中提问:

  

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

你想要的是使你的Runnable线程安全可见性。所以你的问题几乎是矛盾的。引自Java Concurrency in Practice,第3.1.3节。锁定和可见性:

  

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

从第3.1.4节开始。易失性变量:

  

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

     

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

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

但是,如果(由于某些原因我不知道)你不想或不能完全保证线程安全,你可以(自担风险)尝试使用上述规则:如果可以的话整理代码,使得Runnable R字段的更新顺序在所有线程中始终相同,您可以尝试声明最后修改的字段synchronized(或其访问者{{ 1}});从理论上讲,这将保证 all 对其他字段的其他修改随着最后一个字段的更新而变得可见。对我来说,这种诡计显然属于这样的范畴 - 根据上面粗体引用的建议 - 应该避免。

答案 2 :(得分:1)

假设在你的实现中,在将R交给C之前,A需要知道B是用R完成的。

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