Java - 在另一个线程中使用可变对象引用的首选设计?

时间:2012-09-05 20:17:11

标签: java multithreading synchronization thread-safety object-reference

public class ObjectA {

   private void foo() {
       MutableObject mo = new MutableObject();
       Runnable objectB = new ObjectB(mo);
       new Thread(objectB).start();
   }

}

public class ObjectB implements Runnable {

    private MutableObject mo;

    public ObjectB(MutableObject mo) {
        this.mo = mo;
    }

    public void run() {
       //read some field from mo
    }
}

从上面的代码示例中可以看出,我将一个可变对象传递给实现Runnable的类,并将在另一个线程中使用可变对象。这很危险,因为ObjectA.foo()在启动新线程后仍然可以改变可变对象的状态。在这里确保线程安全的首选方法是什么?我是否应该在将MutableObject传递给ObjectB时复制它?可变对象是否应确保内部正确同步?我之前遇到过很多次,尤其是在尝试在许多GUI应用程序中使用SwingWorker时。我通常会尝试确保只将不可变对象引用传递给将在另一个线程中使用它们的类,但有时这可能很困难。

4 个答案:

答案 0 :(得分:2)

这是一个很难回答的问题,不幸的是,答案是“它取决于”。在课程的线程安全方面,您有三种选择:

  1. 让它变得不变,那么你不必担心。但这不是你所要求的。
  2. 使其线程安全。也就是说,在类内部提供足够的并发控制,客户端代码不必担心并发线程修改对象。
  3. 使其不是线程安全的,并强制客户端代码进行某种外部同步。
  4. 你实质上是在问你是否应该使用#2或#3。您担心另一个开发人员使用该类并且不知道它需要外部同步的情况。我喜欢使用JCIP annotations @ThreadSafe @Immutable @NotThreadSafe来记录并发意图。这不是防弹,因为开发人员仍然需要阅读文档,但如果团队中的每个人都理解这些注释并始终如一地应用它们,那么它确实会使事情变得更加清晰。

    对于您的示例,如果您想使该类不是线程安全的,您可以使用AtomicReference使其清晰并提供同步。

    public class ObjectA {
    
      private void foo() {
         MutableObject mo = new MutableObject();
         Runnable objectB = new ObjectB(new AtomicReference<>( mo ) );
         new Thread(objectB).start();
      }
    }
    
    public class ObjectB implements Runnable {
    
      private AtomicReference<MutableObject> mo;
    
      public ObjectB(AtomicReference<MutableObject> mo) {
        this.mo = mo;
      }
    
      public void run() {
       //read some field from mo
       mo.get().readSomeField();
      }
    }
    

答案 1 :(得分:1)

我认为你过于复杂了。如果它是一个例子(一个没有保留引用的局部变量),你应该相信没有人会尝试写入它。如果它更复杂(A.foo()有更多LOC),请创建它以仅传递给线程。

new Thread(new MutableObject()).start();

如果不是(由于初始化),请在块中声明它,使其立即超出范围,甚至可能在单独的私有方法中。

{
   MutableObject mo = new MutableObject();    
   Runnable objectB = new ObjectB(mo);    
   new Thread(objectB).start();    
}
....

答案 2 :(得分:1)

复制对象。您不会有任何奇怪的可见性问题,因为您将副本传递给新的线程。 Thread.start总是在新线程进入其run方法之前发生。如果更改此代码以将对象传递给现有线程,则需要进行适当的同步。我建议使用Java.util.concurrent中的阻塞队列。

答案 3 :(得分:1)

在不知道确切情况的情况下,这个问题很难准确回答。答案完全取决于MutableObject代表什么,有多少其他线程可以同时修改它,以及读取对象的线程是否关心其状态在读取时是否发生变化。

关于线程安全性,内部同步对MutableObject的所有读写操作可证明是“最安全”的事情,但它以性能为代价。如果读写操作的争用率很高,那么您的程序可能会遇到性能问题。你可以通过牺牲一些相互排斥的保证来获得更好的表现 - 这些牺牲是否值得,性能的提升完全取决于你试图解决的具体问题。

你也可以玩一些游戏,了解你如何“内部同步”你的MutableObject,如果这就是你最终要做的事情。如果您还没有,我建议您阅读volatilesynchronized之间的差异,并了解每种方法如何用于确保不同情况下的线程安全。