这里已经有很多答案,但我真的很想深入研究一些细节(就像我的知识一样多)。我强烈建议您运行答案中出现的每个示例,亲自了解事情是如何发生的以及原因。
要了解解决方案,您需要先了解问题。
假设 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());
}
请注意,构造函数是私有的,这是因为我们不想公开另一个公共构造函数并再次考虑类的不变量,因此我们将其设为私有 - 只有我们可以调用它。