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

pet*_*ter 49 java multithreading race-condition

我正在阅读Java Concurrency in Practice4.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;
       }

  }
Run Code Online (Sandbox Code Playgroud)

我不清楚它在哪里说

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

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

Eug*_*ene 41

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

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

假设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();
    }
}
Run Code Online (Sandbox Code Playgroud)

什么变量创建了这个对象的状态?只有两个:x,y.它们是否受到某些同步机制的保护?那么它们是通过内部锁定,通过synchronized关键字 - 至少在setter和getter中.他们在其他地方'感动'吗?当然在这里:

public SafePoint(SafePoint safePoint){
    this(safePoint.x, safePoint.y);
} 
Run Code Online (Sandbox Code Playgroud)

你在这里做的是从你的对象中读取.要使类成为线程安全,您必须协调对它的读/写访问,或者在同一个锁上进行同步.但这里没有发生这样的事情.该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();
}
}
Run Code Online (Sandbox Code Playgroud)

输出很容易这个:

 Copy : SafePoint{X=2, Y=1}
 Original : SafePoint{X=2, Y=2} 
Run Code Online (Sandbox Code Playgroud)

这是逻辑,因为一个Thread更新=写入我们的对象而另一个是从它读取.它们不会在某些常见的锁上同步,从而导致输出.

解?

  • 同步构造函数,以便读取将在同一个锁上同步,但Java中的构造函数不能使用synchronized关键字 - 这当然是逻辑.

  • 可能使用不同的锁,如重入锁(如果不能使用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]);    
}
Run Code Online (Sandbox Code Playgroud)

电话会是这样的:

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

这次代码按预期运行,因为读取和写入在同一个锁上同步,但我们已经删除了构造函数.如果不允许怎么办?

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

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

 public SafePoint(SafePoint safePoint){
     int [] xy = safePoint.getXY();
     this(xy[0], xy[1]);
 }
Run Code Online (Sandbox Code Playgroud)

但编译器不允许这样做.

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

private SafePoint(int [] xy){
    this(xy[0], xy[1]);
}
Run Code Online (Sandbox Code Playgroud)

然后,实际的调用:

public  SafePoint (SafePoint safePoint){
    this(safePoint.getXY());
}
Run Code Online (Sandbox Code Playgroud)

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

  • 你的cloneSafePoint方法应该在'originalSafePoint'而不是'this'上同步吗? (2认同)

fgb*_*fgb 15

私有构造函数是以下的替代:

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

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

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

  • 这部分是正确的.SafePoint是可变的,因此,同样的道理,它可以被另一个线程修改,同时它被构造.原因还在于如何将构造函数链接到public synchronized int [] get(){return new int [] {x,y}; } (4认同)

mab*_*aba 7

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

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

public SafePoint(SafePoint p) {
    int[] values = p.get();
    this(values[0], values[1]);
}
Run Code Online (Sandbox Code Playgroud)

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

call to this must be first statement in constructor
Run Code Online (Sandbox Code Playgroud)

我们也不能使用它:

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

因为那时我们有一个条件,其中值可能在调用之间发生了变化p.get().

所以我们想要从SafePoint和链中捕获值到另一个构造函数.这就是为什么我们将使用私有构造函数捕获习惯用法并将私有构造函数和链中的值捕获到"真正的"构造函数:

private SafePoint(int[] a) {
    this(a[0], a[1]);
}
Run Code Online (Sandbox Code Playgroud)

另请注意

private SafePoint (int [] a) { this (a[0], a[1]); }
Run Code Online (Sandbox Code Playgroud)

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


Bor*_*hov 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
   }
Run Code Online (Sandbox Code Playgroud)

所以我们将在不一致的状态下读取对象.

因此,我们以线程安全的方式创建快照,并将其传递给私有构造函数.堆栈限制保护对阵列的引用,因此没有什么可担心的.

更新 哈!至于技巧,一切都很简单 - 你@ThreadSafe在例子中错过了书中的注释:

@ThreadSafe

公共类SafePoint {}

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