WhyJava Bean Pattern不是线程安全的

时间:2017-06-03 15:50:18

标签: java constructor javabeans builder

Joshua Bloch在Effective Java,第2版中说:

Telescoping构造函数模式的另一个替代方法是JavaBean模式,您可以使用必需参数调用构造函数,然后在以下情况下调用任何可选的setter:

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

这里的问题是,因为对象是在几次调用中创建的,所以在构造过程中它可能处于不一致状态。这还需要花费很多额外的精力来确保线程安全。

我的问题: - 以上代码不是线程安全的吗?我错过了任何基本的东西吗?

先谢谢,

苏里亚

2 个答案:

答案 0 :(得分:4)

您向我们展示的代码只涉及一个线程,因此此代码的线程安全是没有意义的。

如果多个线程可以看到Pizza实例,那么有几件事需要担心:

  1. 在完成初始化之前,另一个线程可以看到Pizza实例吗?

  2. 当另一个线程看到实例时,它是否会观察到属性的正确值?

  3. 第一个问题是通过在完成初始化之前不“发布”对另一个线程的引用来解决。

    可以使用适当的同步机制来解决第二个问题,以确保更改是可见的。这可以通过多种方式完成。例如:

    • 您可以将getter和setter声明为synchronized种方法。
    • 您可以将包含属性值的(私有)变量声明为volatile

    请注意,JavaBean模式没有规定如何构造bean。在您的示例中,您使用no-args构造函数,然后使用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;
         }
    }
    

答案 1 :(得分:1)

作者用文字说:

  

JavaBeans模式排除了使类不可变的可能性,并且需要程序员的额外努力   确保线程安全。

我认为作者强调的事实是,如果对象设计为不可变的,那么提供阻止对象不变性的方法并且可能在线程之间创建一致性问题是没有意义的:这是一旦创建它就永远不需要改变。

你的问题:

  

为什么Java Bean Pattern不是线程安全的?

任何提供改变字段方法的类都不是线程安全的 对于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只需要添加一些Pepperoni单位:

代码可以是:

  if (pizza.isWaitForPepperoni()){
      pizza.addPepperoni(5);
  }

这些陈述不是原子的,所以没有线程安全。

即使已经调用了其中一个线程pizza.addPepperoni(5);,也可以通过两个并发线程调用

pizza.addPepperoni(5);

所以我们应该确保没有其他线程调用pizza.addPepperoni(5)而它不应该(披萨将有太多的意大利辣味香肠)。
例如,通过在Pizza实例上执行synchronized语句:

   synchronized(pizza){
      if (pizza.isWaitForPepperoni()){
          pizza.addPepperoni(5);
      }
   }