私有构造函数,以避免竞争条件

时间:2012-08-19 18:31:07

标签: java multithreading race-condition

我正在阅读Java Concurrency in Practice会话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;
       }

  }

我不清楚它在哪里

  

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

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

9 个答案:

答案 0 :(得分:38)

这里已经有很多答案了,但我真的想深入了解一些细节(尽管我的知识让我感到高兴)。我强烈建议您运行答案中的每个样本,以便亲自了解事情的发生和原因。

要了解解决方案,您需要先了解问题。

假设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中的构造函数不能使用synchronized关键字 - 当然这是逻辑。

  • 可能使用不同的锁,例如Reentrant lock(如果不能使用synchronized关键字)。但它也行不通,因为构造函数中的第一个语句必须是对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());
}

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

答案 1 :(得分:15)

私有构造函数是:

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

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

如果SafePoint(int[])是公共的,则SafePoint类无法保证线程安全,因为数组的内容可以通过另一个持有对同一数组的引用的线程在值之间进行修改x类正在读取ySafePoint

答案 2 :(得分:7)

  

据我所知,它提供了一个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和链中捕获值到另一个构造函数。这就是为什么我们将使用私有构造函数捕获idiom 并将私有构造函数和链中的值捕获到“真正的”构造函数:

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

另请注意

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

在课外没有任何意义。二维点有两个值,而不是数组建议的任意值。它没有检查数组的长度,也没有null。它仅在类中使用,并且调用者知道使用数组中的两个值调用是安全的。

答案 3 :(得分:7)

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

     

公共类SafePoint {}

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

答案 4 :(得分:2)

使用SafePoint的目的是始终提供x&amp;的一致视图。收率

例如,考虑一个SafePoint是(1,1)。一个线程正在尝试读取此SafePoint,而另一个线程正在尝试将其修改为(2,2)。如果安全点不是线程安全的,那么就可以看到SafePoint将是(1,2)(或(2,1))不一致的视图。

提供线程安全一致视图的第一步不是提供对x&amp; amp;的独立访问。 Ÿ;但要提供一种同时访问它们的方法。类似的合同适用于修饰方法。

此时如果在SafePoint中没有实现复制构造函数,那么它就是完全的。但如果我们实施一个,我们需要小心。构造函数无法同步。诸如以下的实现将暴露不一致的状态,因为p.x&amp; 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数组的私有构造函数,该构造函数委托给它(x,y)。 int数组构造函数可以公开,但实际上它将类似于this(x,y)。

答案 5 :(得分:0)

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

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

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

答案 6 :(得分:0)

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

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

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

其次,防止后来的程序员错误地实现复制构造函数,如下所示。这就是为什么作者说:

  

私有构造函数的存在是为了避免在复制构造函数被实现时会发生的竞争条件(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的保护,因此即使其他线程获得p也不会改变!

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

答案 7 :(得分:0)

这意味着,如果您没有私有构造函数并且您以下列方式实现了复制构造函数:

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

现在假设线程A正在访问SafePoint p 正在执行复制构造函数的这个(px,py)指令,并且在不幸的时刻另一个线程B也可以访问SafePoint p 在SafePoint p 上执行setter set(int x,int y)。由于您的复制构造函数直接访问 p x y 实例变量而没有正确锁定,因此可能会看到SafePoint p的状态不一致

私有构造函数通过同步的getter访问 p 的变量 x y ,以确保您看到一致SafePoint的状态 p

答案 8 :(得分:0)

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

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

然后尝试制作副本构造函数。

方法1:

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

上述方法的问题在于它将使我们的类没有螺纹安全性

如何?

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

因此,我们的方法不是线程安全的,因为构造函数未同步。

方法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对象。