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

我正在阅读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数组,而不是每个单独的getter,所以调用者会看到一致的值,但为什么是私有构造函数? 这里有什么窍门

这里已经有了一堆答案,但是我真的想深入一些细节(就像我的知识让我一样)。 我强烈build议您运行答案中的每个样本,以了解事情正在发生的情况以及原因。

要了解解决scheme,您需要首先了解问题。

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

什么variables创build这个对象的状态? 其中只有两个:x,y。 他们受到某种同步机制的保护吗? 那么他们是由固有的locking,通过同步关键字 – 至less在setters和getters。 他们在其他地方“感动”吗? 当然这里:

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

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

让我们试试这个:

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

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

    lock.lock()//其中locking是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()); } 

这次代码按预期运行,因为读和写在同一个锁上是同步的,但是我们已经丢弃了构造函数 。 如果这不被允许呢?

我们需要find一种方法来读取和写入同一锁上同步的SafePoint。

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

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

但编译器不允许这样做。

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

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

然后,实际的调用:

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

请注意,构造函数是私有的,这是因为我们不想公开另一个公共构造函数,并再次考虑类的不variables,因此我们使它成为私有的 – 只有我们可以调用它。

私有构造函数可以替代:

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

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

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

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

我们无法将public SafePoint(SafePoint p){ 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 } 

所以我们将读取处于不一致状态的对象。

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

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

@ThreadSafe

公共课SafePoint {}

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

我明白,它提供了一个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(px, py); } 

因为那么我们有一个条件,在调用p.get()之间值可能已经改变了。

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

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

另请注意

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

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

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

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

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

此时如果复制构造函数没有在SafePoint内部实现,那么它是完全的。 但是,如果我们实施一个,我们需要小心。 构造函数不能同步。 像下面这样的实现将暴露一个不一致的状态,因为px&py是独立访问的。

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

但以下不会打破线程安全。

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

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

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

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

私有构造函数将允许您以线程不安全的方式构build自己的方法(即分别检索x,y,构build数组并传递它)

私人SafePoint(int [] a)提供了两个function:

首先,防止他人使用下面的构造函数,因为其他线程可以获得对数组的引用,并且可能在构造时改变数组

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

看作者的实现:你不必担心p被其他线程修改了,因为p.get()返回一个新的副本 ,p.get()也被p的这个保护,所以p不会改变,甚至获得通过其他线程!

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

这意味着什么,如果你没有一个私有的构造函数,并且按照以下的方式实现拷贝构造函数:

 public SafePoint(SafePoint p) { this(px, py); } 

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

在私人构造函数通过getter来访问p的variablesxy的同时,保证看到SafePoint p的一致状态。