ここにはすでにたくさんの答えがありますが、いくつかの詳細に飛び込みたいと思います(私の知識が許す限り)。ここにある回答にある各サンプルを実行して、状況とその理由を自分で確認することを強くお勧めします。
ソリューションを理解するには、まず問題を理解する必要があります。
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();
}
}
このオブジェクトの状態を作成する変数は何ですか? それらのうちの2つだけ:x、y。それらは何らかの同期メカニズムによって保護されていますか? まあ、それらは Synchronized キーワードを介した組み込みロックによるものです - 少なくともセッターとゲッターでは。彼らは他のどこかに「触れた」のですか?もちろんここに:
public SafePoint(SafePoint safePoint){
this(safePoint.x, safePoint.y);
}
ここで行っているのは、オブジェクトからの読み取りです。クラスをスレッドセーフにするには、クラスへの読み取り/書き込みアクセスを調整するか、同じロックで同期する必要があります。しかし、ここではそのようなことは起きていません。setXYメソッドは確かに同期されていますが、クローン コンストラクターは同期されていないため、これら 2 つの呼び出しは非スレッドセーフな方法で行うことができます。このクラスにブレーキをかけることはできますか?
これを試してみましょう:
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}
これはロジックです。1 つのスレッドがオブジェクトに更新 = 書き込みを行い、もう 1 つのスレッドがオブジェクトから読み取りを行っているからです。それらは、いくつかの共通ロック、したがって出力で同期しません。
解決?
読み取りが同じロックで同期されるように同期コンストラクターを使用しますが、Java のコンストラクターは synchronized キーワードを使用できません。これはもちろんロジックです。
Reentrant ロックのような別のロックを使用することもできます (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]);
}
呼び出しは次のようになります。
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());
}
コンストラクターがプライベートであることに注意してください。これは、さらに別のパブリック コンストラクターを公開して、クラスの不変条件についてもう一度考えたくないためです。したがって、コンストラクターをプライベートにし、呼び出すことができるのは自分だけです。