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

WhyJava Bean Pattern is not threadsafe

Joshua Bloch 在 Effective Java,第 2 版中指出:

伸缩构造函数模式的一个替代方法是 JavaBean 模式,您可以在其中调用带有强制参数的构造函数,然后在以下之后调用任何可选的设置器:

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

这里的问题是,由于对象是通过多次调用创建的,因此在构建过程中可能处于不一致状态。这也需要付出很多额外的努力来保证线程安全。

我的问题:- 上面的代码不是线程安全的吗?我缺少任何基本的东西吗?

提前致谢,

苏里亚

作者如是说:

JavaBeans pattern precludes the possibility of making a class immutable and requires a added effort on the part of the programmer to ensure thread safety.

我认为作者强调了这样一个事实,即提供防止对象不变性的方法是没有意义的,并且 可能 如果您将对象设计为不可变:一旦创建就永远不需要更改。

您的问题:

Why Java Bean Pattern is not threadsafe ?

任何提供改变字段方法的class都不是线程安全的。
这对于 JavaBeans 方法(通常不使用防御性副本)是正确的,但对于任何可变 class.

也是如此

如果您在线程之间没有竞争条件的上下文中使用它,那么操作非线程安全 class 并不是一个必要的问题。
例如这段代码是线程安全的:

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() 的调用才会创建和 returns Pizza 对象。
以前的调用操作一个 Builder 对象和 return 一个 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 必须添加一些意大利辣香肠一次:

代码可以是:

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

这些语句不是原子的,因此不是线程安全的。

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

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

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

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

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

  1. 另一个线程能否在您完成初始化之前看到 Pizza 实例?

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

第一个问题是在完成初始化之前不"publishing"引用另一个线程。

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

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

请注意,JavaBean 模式并未规定 bean 的构造方式。在您的示例中,您使用无参数构造函数,然后使用设置器设置字段。您还可以实现一个构造函数,它允许您传递为属性提供(非默认)初始值的参数。

This also requires a lot of extra effort to ensure thread safety

不是真的。在这种情况下,使 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;
     }
}