为什么Java Bean模式不是线程安全的。

4

Joshua Bloch在《Effective Java, 2nd Edition》中提到:

替代 Telescoping Constructor Pattern 的一种方法是使用JavaBean模式,在构造函数中传入必须的参数,然后再调用任意可选的setter方法进行设置:

Pizza pizza = new Pizza(12);
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

这里的问题在于,因为对象是通过多个调用创建的,所以在构建过程中可能处于不一致的状态。这也需要很多额外的工作来确保线程安全。

我的问题是:上述代码不安全吗?我有没有漏掉什么基本的东西?

提前感谢您的帮助,

Surya


我的理解是,在方法中(比如createPizza()方法)我们将创建新的实例。当多个线程调用createPizza()方法时,它们拥有自己的Pizza实例,并且是线程安全的。不是吗? - Surya P
是的,你的理解是正确的! - Minh Kieu
它说什么?也许你误解了吗? - Minh Kieu
由于您正在阅读《Effective Java》,您会发现您的问题的答案在“并发性”一章中得到了很好的呈现。 - scottb
当然,我会仔细阅读它。感谢所有帮助我理解这个问题的人。 - Surya P
显示剩余2条评论
2个回答

4
你所展示的代码仅涉及一个线程,因此这段代码的线程安全性无关紧要。
如果多个线程可以看到“Pizza”实例,则需要担心以下几点: 1. 在你完成初始化之前,其他线程能否看到“Pizza”实例? 2. 当其他线程看到该实例时,它是否会观察到属性的正确值?
第一个问题可以通过在完成初始化之前不将引用“发布”给另一个线程来解决。
第二个问题可以通过使用适当的同步机制确保更改可见来解决。这可以通过以下多种方式来完成: - 可以将getter和setter声明为同步方法。 - 可以将持有属性值的(私有)变量声明为“volatile”。
请注意,JavaBean模式并未规定如何构造bean。在您的示例中,您使用一个无参构造函数,然后使用setter设置字段。还可以实现一个构造函数,允许您传递参数以提供属性的(非默认)初始值。
引用的话说:“这也需要大量的额外工作来确保线程安全”,其实不是这样的。在线程安全的上下文中,使getter和setter线程安全只需进行小幅更改即可,例如:
public class Pizza {
     private boolean cheese;

     public synchronized /* added */ void setCheese(boolean cheese) {
         this.cheese = cheese;
     }

     public synchronized /* added */ boolean isCheese() {
         return cheese;
     }
}

你能否添加一个使setter/getter线程安全的例子?在它们上面添加synchronized关键字很简单,但是如果你将所有的setter视为初始化的一部分,那么你需要确保在任何其他Setter被调用时,没有任何Setter被调用。 - Robert
我不明白你的观点。添加synchronized就可以实现这个功能。 - Stephen C

1
作者的文字内容如下:
JavaBeans模式排除了使类成为不可变的可能性,并需要程序员额外的努力来确保线程安全。
我认为作者强调的是,如果您的对象被设计为不可变的(即创建后永远不需要更改),则提供防止对象不可变性的方法是没有意义的,并且可能在线程之间创建一致性问题。
你的问题是:
为什么Java Bean模式不是线程安全的?
任何提供修改字段方式的类都不是线程安全的。这适用于JavaBeans方法(通常不使用防御性副本),但对于任何可变类也是如此。
操作不安全的类并不一定是问题,如果您将其用于没有线程之间的竞争条件的上下文中。例如,此代码是线程安全的:
Pizza pizza = new Pizza(12);
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

因为Pizza实例没有被声明为共享变量(实例或静态字段),而是在更受限制的范围内声明和使用(可能是方法,但也可能是初始化块)。
建造者模式提供了一种构建不可变且因此按定义是线程安全的对象的方式。
例如,通过使用构建器来创建Pizza:
Pizza pizza = new Pizza.Builder().cheese(true).pepperoni(true).bacon(true).build();

只有调用build()方法才会创建并返回Pizza对象。
之前的调用操作的是Builder对象,并返回Builder
因此,如果对象是不可变的,您无需担心同步这些调用:

pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

由于这些方法不需要提供,所以它们无法被调用。
关于如何拥有线程安全的JavaBeans
如果您在一个上下文中,Pizza实例可能会在多个线程之间共享,那么这些调用应该以同步方式进行:
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);

这些方法可以声明为synchronized,或者Pizza字段可以是易失性的,但这可能不足够。
实际上,如果Pizza应根据其自身状态或甚至根据另一个对象更改其状态,我们还应该同步整个逻辑:在Pizza的状态修改之前进行检查。
例如,假设Pizza只需要添加一次Pepperoni:
代码可能是:
  if (pizza.isWaitForPepperoni()){
      pizza.addPepperoni(5);
  }

这些语句不是原子的,因此不是线程安全的。
即使其中一个线程已经调用了pizza.addPepperoni(5);,“pizza.addPepperoni(5);”也可能被两个并发线程调用。
因此,我们应确保在不应该调用pizza.addPepperoni(5)时,没有其他线程调用它(比如披萨上会有过多的意大利辣肠)。例如,在Pizza实例上执行同步语句:
   synchronized(pizza){
      if (pizza.isWaitForPepperoni()){
          pizza.addPepperoni(5);
      }
   }

我认为他并不是在问那个。他并不是在问如何使上面的代码线程安全。 - Minh Kieu
@Minh Kieu 你是对的。我编辑了以设置上下文。 - davidxxx
Pizza pizza = new Pizza.Builder().cheese(true).pepperoni(true).bacon(true).build(); 这不是一个调用。这是5个调用。 - Angel O'Sphere
@Angel O'Sphere,只有对 build() 的调用才会创建并返回披萨对象。之前的调用仅操作 Builder 对象并返回 Builder。 - davidxxx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接