私有构造函数来避免竞争条件(Private constructor to avoid race co

2019-06-27 04:26发布

我读这本书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;
       }

  }

我不明白的地方说:

私有构造函数的存在是为了避免如果拷贝构造函数是因为这(PX,PY)实现了不会发生竞争状态; 这是私有构造捕获成语(Bloch和Gafter,2005)的一个例子。

据我所知,它提供了一个getter一次检索x和y一个阵列,而不是针对每个单独的吸气剂,所以调用者会看到一致的价值,但为什么私有构造? 什么是这里的窍门

Answer 1:

已经有一堆答案在这里,但我真的想深入到一些细节(尽可能我的知识,让我们我)。 我强烈建议你运行每个样品是在座的各位在回答自己看看事情是如何发生的,以及为什么。

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

假设还原点类实际上是这样的:

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关键字 - 至少在getter和setter方法。 难道他们“感动”别的地方? 当然,在这里:

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关键字 - 这是当然的逻辑。

  • 可以使用不同的锁,如重入锁(如果同步关键字不能使用)。 但它也不会起作用,因为在构造函数中的第一条语句必须是本/超的电话 。 如果我们实现一个不同的锁,然后在第一行必须是这样的:

    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());
 }

这一次的代码运行正常,因为读和写是在相同的锁同步,但我们已经放弃了构造函数 。 如果这是不允许的?

我们需要找到一种方法来读取和写入到还原点在同一个锁同步。

理想情况下,我们希望是这样的:

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

但是编译器不允许这样。

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

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

然后,实际invokation:

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

请注意,构造函数是私有的,这是因为我们不希望暴露又一公共构造和重新考虑之类的不变量,因此,我们将其变为私有-只有我们可以调用它。



Answer 2:

私人构造函数是一种替代方案:

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

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

如果SafePoint(int[])分别为公共则SafePoint类不能保证线程安全,因为数组的内容可以被修改,通过另一个线程保持到同一阵列的引用,的值之间xy被读取由SafePoint类。



Answer 3:

在Java中构造函数不能同步。

我们无法实现public SafePoint(SafePoint p){ this (px, py); } { this (px, py); }因为

由于我们不同步(可以因为我们不是在构造函数),构造函数的执行过程中,有人可能会调用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数组作为参数将是公共受保护的构造函数,类将不再是线程安全的,因为数组的内容可能会改变以同样的方式作为还原点类(即某人可能在改变构造函数执行)!



Answer 4:

据我所知,它提供了一个getter一次检索x和y一个阵列,而不是针对每个单独的吸气剂,所以调用者会看到一致的价值,但为什么私有构造? 这里有什么诀窍?

我们想在这里什么是构造函数的调用链接,以避免重复代码。 理想的情况是这样的事情是我们所希望的:

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()

所以,我们想捕捉从还原点和链条的值,以另一个构造。 这就是为什么我们将使用私有的构造捕获成语和私有构造和链“真正的”构造捕捉值:

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

另外请注意,

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

不会使类以外的任何意义。 A 2-d点具有两个值,而不是任意值像阵列暗示。 它没有检查阵列的长度,也没有,这是不null 。 它只是使用了类内和调用者知道它是安全与数组两个值调用。



Answer 5:

使用还原点的目的是要始终提供X&Y的一致视图。

例如,考虑一个还原点是(1,1)。 并且一个线程试图,而另一个线程试图修改它(2,2)读取该还原点。 有安全点没有线程安全的它本来可以看到的观点,其中还原点将是(1,2)(或(2,1)),这是不一致的。

针对提供一个线程安全一致视图第一步是不提供给X和Y独立的访问; 而是提供在同一时间访问他们两个的方法。 类似的合同适用于改性剂的方法。

在这一点上,如果一个拷贝构造函数是不是里面还原点实现,那么它完全是。 但是,如果我们实现一个我们必须要小心。 构造函数不能同步。 实现,如以下将暴露因为PX&PY被独立地访问的不一致状态。

   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数组构造可以公开,但随后在效果这将是与此类似(X,Y)。



Answer 6:

构造函数是不应该这个类以外的地方使用。 客户端不应该是能够建立一个数组并把它传递给这个构造。

所有其他公共构造暗示还原点的get方法会被调用。

私人构造将允许你建立你自己的,也许线程不安全的方式(通过检索X即,y分开,建立一个数组并传递)



Answer 7:

私有还原点(INT []一)提供两个功能:

首先,防止从使用下面的构造,becasue其他线程能够获得裁判到阵列他人,并可能改变该阵列在构造

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

其次,为防止后来的程序员错误地执行拷贝构造函数类似以下。 这就是为什么笔者说:

私有构造函数的存在是为了避免如果拷贝构造函数是因为这实现了不会发生竞争状态(PX,PY)

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

见笔者的实现:你不必担心p被其他线程修改,作为p.get()返回一个新的副本 ,也p.get()由普的这种谨慎,因此P也不会发生变化,也能获得其它线程!

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


Answer 8:

这是什么意思是,如果你没有一个私有构造函数,你实现下列方式拷贝构造函数:

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

现在假设线程A是具有上述复制构造的本(PX,PY)的指令,并在不走运的定时也能够访问还原点p另一个线程B正在执行访问还原点p上执行还原点设定器组(INT的x,int y)对 。 由于您的拷贝构造函数直接访问pxy实例变量没有适当的锁定也能看到还原点P的不一致的状态。

凡为私有构造函数访问它是同步的,所以你可以保证看到还原点P的一致状态P的变量x和通过吸气



Answer 9:

我们的要求是:我们希望有一个拷贝构造函数像下面(在同一时间,确保一流的仍然是线程安全的):

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(px, py) px将是新价值的同时, py仍然是旧的。

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

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

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

这是线程安全的,因为p.get()是由内部锁同步。 因此,虽然p.get()执行,因为这两个getter和setter由相同的内在锁保护其他线程无法执行的制定者。

但不幸的是,编译器将不允许我们这样做,因为this(px, py)应该是第一个发言。

这给我们带来了我们的最终办法。

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

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

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

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

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



文章来源: Private constructor to avoid race condition