【问题标题】:Private constructor to avoid race condition避免竞争条件的私有构造函数
【发布时间】:2012-08-19 18:31:06
【问题描述】:

我正在阅读本书Java Concurrency in Practice session 4.3.5

  @ThreadSafe
  public class SafePoint{

       @GuardedBy("this") private int x,y;

       private SafePoint (int [] a) { this (a[0], a[1]); }

       public SafePoint(SafePoint p) { this (p.get()); }

       public SafePoint(int x, int y){
            this.x = x;
            this.y = y;
       }

       public synchronized int[] get(){
            return new int[] {x,y};
       }

       public synchronized void set(int x, int y){
            this.x = x;
            this.y = y;
       }

  }

我不清楚它在哪里写的

私有构造函数的存在是为了避免复制构造函数实现为 this (p.x, p.y); 时会发生的竞争条件;这是私有构造函数捕获习语的一个示例(Bloch 和 Gafter,2005)。

我知道它提供了一个 getter 来一次在数组中检索 x 和 y,而不是为每个单独的 getter,所以调用者会看到一致的值,但为什么是私有构造函数?这里有什么诀窍

【问题讨论】:

  • 这只是private,因为他们不想让其他人使用它;-)
  • @user1389813 您应该注意到,如果将构造函数重构为例如方法,则可以轻松避免这种模式。另请参阅我的回答;)

标签: java multithreading race-condition


【解决方案1】:

这里已经有很多答案,但我真的很想深入了解一些细节(就我的知识而言,让我吧)。我强烈建议您运行答案中出现的每个示例,以亲自了解事情的发生方式和原因。

要了解解决方案,首先要了解问题。

假设 SafePoint 类实际上如下所示:

class SafePoint {
    private int x;
    private int y;

    public SafePoint(int x, int y){
        this.x = x;
        this.y = y;
    }

    public SafePoint(SafePoint safePoint){
        this(safePoint.x, safePoint.y);
    }

    public synchronized int[] getXY(){
        return new int[]{x,y};
    }

    public synchronized void setXY(int x, int y){
        this.x = x;
        //Simulate some resource intensive work that starts EXACTLY at this point, causing a small delay
        try {
            Thread.sleep(10 * 100);
        } catch (InterruptedException e) {
         e.printStackTrace();
        }
        this.y = y;
    }

    public String toString(){
      return Objects.toStringHelper(this.getClass()).add("X", x).add("Y", y).toString();
    }
}

什么变量创建了这个对象的状态?只有两个:x,y。它们是否受到某种同步机制的保护?好吧,它们是通过内在锁,通过 synchronized 关键字 - 至少在 setter 和 getter 中。他们在其他任何地方都被“感动”了吗?当然在这里:

public SafePoint(SafePoint safePoint){
    this(safePoint.x, safePoint.y);
} 

你在这里所做的是读取你的对象。对于一个线程安全的类,你必须协调对它的读/写访问,或者在同一个锁上同步。但是这里没有发生这样的事情。 setXY 方法确实是同步的,但是克隆构造函数不是,因此调用这两个可以以非线程安全的方式完成。我们可以刹车吗?

让我们试试这个:

public class SafePointMain {
public static void main(String[] args) throws Exception {
    final SafePoint originalSafePoint = new SafePoint(1,1);

    //One Thread is trying to change this SafePoint
    new Thread(new Runnable() {
        @Override
        public void run() {
            originalSafePoint.setXY(2, 2);
            System.out.println("Original : " + originalSafePoint.toString());
        }
    }).start();

    //The other Thread is trying to create a copy. The copy, depending on the JVM, MUST be either (1,1) or (2,2)
    //depending on which Thread starts first, but it can not be (1,2) or (2,1) for example.
    new Thread(new Runnable() {
        @Override
        public void run() {
            SafePoint copySafePoint = new SafePoint(originalSafePoint);
            System.out.println("Copy : " + copySafePoint.toString());
        }
    }).start();
}
}

输出很容易就是这个:

 Copy : SafePoint{X=2, Y=1}
 Original : SafePoint{X=2, Y=2} 

这是逻辑,因为一个线程更新=写入我们的对象,另一个正在读取它。它们不会在某些公共锁上同步,因此不会同步输出。

解决方案?

  • 同步构造器,这样读取将在同一个锁上同步,但是Java中的构造器不能使用同步关键字——这当然是逻辑。

  • 可以使用不同的锁,比如可重入锁(如果同步关键字不能使用)。但它也不起作用,因为构造函数中的第一条语句必须是对 this/super 的调用。如果我们实现不同的锁,那么第一行必须是这样的:

    lock.lock() //这里的lock是ReentrantLock,编译器不会因为上面提到的原因允许这个。

  • 如果我们将构造函数设为方法会怎样?当然可以!

查看此代码示例

/*
 * this is a refactored method, instead of a constructor
 */
public SafePoint cloneSafePoint(SafePoint originalSafePoint){
     int [] xy = originalSafePoint.getXY();
     return new SafePoint(xy[0], xy[1]);    
}

调用看起来像这样:

 public void run() {
      SafePoint copySafePoint = originalSafePoint.cloneSafePoint(originalSafePoint);
      //SafePoint copySafePoint = new SafePoint(originalSafePoint);
      System.out.println("Copy : " + copySafePoint.toString());
 }

这一次代码按预期运行,因为读取和写入同步在同一个锁上,但是我们删除了构造函数。如果不允许这样做呢?

我们需要找到一种方法在同一个锁上同步读取和写入 SafePoint。

理想情况下,我们会想要这样的东西:

 public SafePoint(SafePoint safePoint){
     int [] xy = safePoint.getXY();
     this(xy[0], xy[1]);
 }

但编译器不允许这样做。

我们可以通过调用 *getXY 方法来安全地读取,因此我们需要一种方法来使用它,但是我们没有一个构造函数来接受这样的参数 - 创建一个。

private SafePoint(int [] xy){
    this(xy[0], xy[1]);
}

然后,实际的调用:

public  SafePoint (SafePoint safePoint){
    this(safePoint.getXY());
}

注意构造函数是私有的,这是因为我们不想暴露另一个公共构造函数并再次考虑类的不变量,因此我们将其设为私有 - 只有我们可以调用它。

【讨论】:

  • 您的 cloneSafePoint 方法是否应该在 'originalSafePoint' 而不是 'this' 上同步?
  • @fgb 该死的,好收获!我需要做的就是以线程安全的方式读取参数 x 和 y。我可以为此调用 getXY。但是在 originalSafePoint 中同步也可以
  • 把构造函数public SafePoint(SafePoint safePoint)...改成public SafePoint(SafePoint safePoint){ synchronized (safePoint) { this.x = safePoint.x; this.y = safePoint.y; } }怎么样
  • 为什么我们不能有复制构造函数的主体,例如:this(safePoint.getXY()[0], safePoint.getXY()[1]);
  • 这是我在 StackOverflow 上找到的最好的答案之一,它易于阅读和理解。谢谢!
【解决方案2】:

私有构造函数可以替代:

public SafePoint(SafePoint p) {
    int[] a = p.get();
    this.x = a[0];
    this.y = a[1];
}

但允许构造函数链接以避免重复初始化。

如果SafePoint(int[]) 是公共的,那么SafePoint 类不能保证线程安全,因为数组的内容可以被另一个持有对同一数组的引用的线程在x 的值之间修改ySafePoint 类读取。

【讨论】:

  • 这是部分正确的。 SafePoint 是可变的,因此同理,它可以在构造时被另一个线程修改。原因还在于构造函数如何链接到 public synchronized int[] get() { return new int[] { x, y }; }
【解决方案3】:

Java 中的构造函数无法同步。

我们不能将public SafePoint(SafePoint p) 实现为{ this (p.x, p.y); },因为

因为我们没有同步(也不能像我们在构造函数中那样), 在构造函数执行期间,可能有人从不同的线程调用SafePoint.set()

public synchronized void set(int x, int y){
        this.x = x; //this value was changed
-->     this.y = y; //this value is not changed yet
   }

所以我们会读取不一致状态的对象。

因此,我们以线程安全的方式创建快照,并将其传递给私有构造函数。堆栈限制保护对数组的引用,因此无需担心。

更新 哈!至于诀窍,一切都很简单-您在示例中错过了书中的@ThreadSafe 注释:

@ThreadSafe

公共类安全点 { }

因此,如果将 int 数组作为参数的构造函数将是 publicprotected,则该类将不再是线程安全的,因为数组可能会以与 SafePoint 类相同的方式更改(即有人可能会在构造函数执行期间更改它)!

【讨论】:

  • +1 我认为你是最接近真实答案的,强调构造函数不能同步的事实。
  • 但是构造函数体可以同步
  • @gstackoverflow 是的,这是正确的,但不可能将链构造函数调用放在同步块中,因为它不会是 constucot 的第一行
【解决方案4】:

我知道它提供了一个 getter 来一次在数组中检索 x 和 y,而不是为每个单独的 getter,所以调用者会看到一致的值,但为什么是私有构造函数?这里有什么诀窍?

我们在这里想要的是链接构造函数调用以避免代码重复。理想情况下,我们想要的是这样的:

public SafePoint(SafePoint p) {
    int[] values = p.get();
    this(values[0], values[1]);
}

但这不起作用,因为我们会得到一个编译器错误:

call to this must be first statement in constructor

我们也不能用这个:

public SafePoint(SafePoint p) {
    this(p.get()[0], p.get()[1]); // alternatively this(p.x, p.y);
}

因为那时我们有一个条件,即在调用 p.get() 之间值可能已更改。

所以我们想从 SafePoint 捕获值并链接到另一个构造函数。这就是为什么我们将使用私有构造函数捕获惯用语并捕获私有构造函数中的值并链接到“真正的”构造函数:

private SafePoint(int[] a) {
    this(a[0], a[1]);
}

还要注意

private SafePoint (int [] a) { this (a[0], a[1]); }

在课堂之外没有任何意义。一个二维点有两个值,而不是数组所暗示的任意值。它没有检查数组的长度,也没有检查它不是null。它只在类中使用,调用者知道使用数组中的两个值进行调用是安全的。

【讨论】:

  • 还有更多内容;如果构造函数被公开,该类将不再是线程安全的。
【解决方案5】:

使用 SafePoint 的目的是始终提供一致的 x 和 y 视图。

例如,假设 SafePoint 为 (1,1)。一个线程正在尝试读取此 SafePoint,而另一个线程正在尝试将其修改为 (2,2)。如果安全点不是线程安全的,则可能会看到 SafePoint 为 (1,2)(或 (2,1))的视图,这是不一致的。

提供线程安全一致视图的第一步不是提供对 x 和 y 的独立访问;但要提供一种同时访问它们的方法。类似的契约也适用于修饰符方法。

此时,如果在 SafePoint 内部没有实现复制构造函数,那么它就完全实现了。但是,如果我们确实实施了一个,我们需要小心。构造函数不能同步。如下实现会暴露不一致的状态,因为 p.x & p.y 是独立访问的。

   public SafePoint(SafePoint p){
        this.x = p.x;
        this.y = p.y;
   }

但跟随不会破坏线程安全。

   public SafePoint(SafePoint p){
        int[] arr = p.get();
        this.x = arr[0];
        this.y = arr[1];
   }

为了重用代码,实现了一个接受 int 数组的私有构造函数,该构造函数委托给 this(x, y)。 int 数组构造函数可以公开,但实际上它类似于 this(x, y)。

【讨论】:

    【解决方案6】:

    构造函数不应该在这个类之外使用。客户端不应该能够构建一个数组并将其传递给这个构造函数。

    所有其他公共构造函数都暗示将调用 SafePoint 的 get 方法。

    私有构造函数将允许您以一种可能是线程不安全的方式构建自己的构造函数(即通过分别检索 x、y、构建一个数组并传递它)

    【讨论】:

      【解决方案7】:

      私有 SafePoint(int[] a) 提供两个功能:

      首先,防止别人使用下面的构造函数,因为其他线程可以获取到数组的ref,并且在构造的时候可能会改变数组

      int[] arr = new int[] {1, 2};
      // arr maybe obtained by other threads, wrong constructor
      SafePoint safepoint = new SafePoint(arr); 
      

      其次,防止以后的程序员错误地像下面那样实现复制构造函数。所以作者说:

      私有构造函数的存在是为了避免复制构造函数被实现为 this(p.x, p.y) 时会发生的竞争条件

      //p may be obtined by other threads, wrong constructor
      public SafePoint(SafePoint p) { this(p.x, p.y);}
      

      查看作者的实现:您不必担心p 被其他线程修改,因为 p.get() 返回一个 新副本,也是 p。 get() 被 p 的 this 守护,所以 p 不会被改变,即使被其他线程获取!

      public SafePoint(SafePoint p) {
          this(p.get());
      }
      public synchronized int[] get() {
          return new int[] {x, y};
      }
      

      【讨论】:

        【解决方案8】:

        这是什么意思,如果你没有私有构造函数并且你通过以下方式实现复制构造函数:

        public SafePoint(SafePoint p) {
            this(p.x, p.y);
        }
        

        现在假设线程 A 可以访问 SafePoint p 在复制构造函数的 this(p.x, p.y) 指令之上执行,并且在不幸的时间另一个线程 B 也可以访问 SafePoint p 在 SafePoint p 上执行 setter set(int x, int y)。由于您的复制构造函数在没有适当锁定的情况下直接访问 pxy 实例变量,因此可能会看到 SafePoint p 的状态不一致

        当私有构造函数通过同步的 getter 访问 p 的变量 xy 时,可以保证看到一致SafePoint 的状态p

        【讨论】:

          【解决方案9】:

          我们的要求是:我们希望有一个像下面这样的复制构造函数(同时确保类仍然是线程安全的):

          public SafePoint(SafePoint p){
              // clones 'p' passed a parameter and return a new SafePoint object.
          }
          

          那么让我们尝试制作复制构造函数。

          方法一:

          public SafePoint(SafePoint p){
              this(p.x, p.y);
          }
          

          上述方法的问题是它会渲染我们的类 NOT THREAD SAFE

          如何?

          因为构造函数不是同步的,这意味着两个线程可能同时作用于同一个对象(一个线程可能使用它的复制构造函数克隆这个对象,而另一个线程可能会调用对象的 setter 方法)。如果发生这种情况,调用 setter 方法的线程可能已经更新了 x 字段(但尚未更新 y 字段),从而使对象处于不一致的状态。现在,如果另一个线程(正在克隆对象)执行(并且它可以执行,因为构造函数未通过内部锁同步),则复制构造函数 this(p.x, p.y)p.x 将是新值,而 @ 987654327@ 仍然是旧的。

          所以,我们的方法不是线程安全的,因为构造函数没有同步。

          方法 2:(试图使方法 1 线程安全)

          public SafePoint(SafePoint p){
              int[] temp = p.get();
              this(temp[0], temp[1]);
          }
          

          这是线程安全的,因为p.get() 是通过内部锁同步的。因此,当 p.get() 执行时,其他线程无法执行 setter,因为 getter 和 setter 都被同一个内部锁保护。

          但不幸的是,编译器不允许我们这样做,因为this(p.x, p.y) 应该是第一条语句。

          这将我们带到我们的最终方法。

          方法3:(解决方法2的编译问题)

          public SafePoint(SafePoint p){
              this(p.get());
          }
          
          private SafePoint(int[] a){
              this(a[0], a[1]);
          }
          

          通过这种方法,我们可以保证我们的类是线程安全的,并且我们有我们的复制构造函数。

          剩下的最后一个问题是为什么第二个构造函数是私有的? 这仅仅是因为我们创建这个构造函数只是为了我们的内部目的,我们不希望客户端通过调用这个方法来创建 SafePoint 对象。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2015-01-30
            • 2010-09-25
            • 2010-09-25
            • 2019-06-12
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多