用于避免争用条件的私有构造函数

2022-08-31 17:18:37

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

  @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 and Gafter,2005)。

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


答案 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。它们是否受某种同步机制保护?好吧,它们通过内在锁定,通过同步关键字 - 至少在设置者和获取器中。他们是否在其他地方被“触摸”了?当然在这里:

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} 

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

溶液?

  • 同步构造函数使得读取将在同一个锁上进行同步,但是Java中的构造函数不能使用synced关键字 - 这当然是逻辑。

  • 可以使用不同的锁,如重入锁(如果同步关键字不能使用)。但它也不起作用,因为构造函数中的第一个语句必须是对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());
}

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


答案 2

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

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

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

如果是公共的,则该类无法保证线程安全,因为数组的内容可以被另一个线程在类的值之间修改,该线程持有对同一数组的引用。SafePoint(int[])SafePointxySafePoint