57

私は本の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;
       }

  }

どこに書いてあるかはっきりしない

プライベート コンストラクターは、コピー コンストラクターが this (px, py) として実装された場合に発生する競合状態を回避するために存在します。これは、プライベート コンストラクター キャプチャ イディオムの例です (Bloch and Gafter、2005 年)。

x と y の両方を一度に配列で取得するゲッターを提供することを理解しています。ここでのトリックは何ですか

4

9 に答える 9

54

ここにはすでにたくさんの答えがありますが、いくつかの詳細に飛び込みたいと思います(私の知識が許す限り)。ここにある回答にある各サンプルを実行して、状況とその理由を自分で確認することを強くお勧めします。

ソリューションを理解するには、まず問題を理解する必要があります。

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

コンストラクターがプライベートであることに注意してください。これは、さらに別のパブリック コンストラクターを公開して、クラスの不変条件についてもう一度考えたくないためです。したがって、コンストラクターをプライベートにし、呼び出すことができるのは自分だけです。

于 2012-08-20T12:19:28.927 に答える
16

プライベート コンストラクターは、次の代替手段です。

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

ただし、コンストラクターの連鎖を許可して、初期化の重複を回避します。

SafePoint(int[])public の場合、クラスの値とクラスによって読み取られるSafePoint値の間で、同じ配列への参照を保持する別のスレッドによって配列の内容が変更される可能性があるため、クラスはスレッドセーフを保証できません。xySafePoint

于 2012-08-19T19:31:43.500 に答える
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
   }

そのため、一貫性のない状態でオブジェクトを読み取ります。

代わりに、スレッドセーフな方法でスナップショットを作成し、それをプライベート コンストラクターに渡します。スタックの制限によって配列への参照が保護されるため、心配する必要はありません。

更新 ハ!トリックに関しては、すべてが単純です -@ThreadSafeあなたの例では本の注釈を見逃しています:

@スレッドセーフ

パブリック クラス SafePoint { }

そのため、 int 配列を引数として取るコンストラクターがpublicまたはprotectedの場合、配列の内容が SafePoint クラスと同じように変更される可能性があるため (つまり、誰かが変更中に変更する可能性があるため)、クラスはスレッドセーフではなくなります。コンストラクターの実行)!

于 2012-08-19T19:36:47.850 に答える
7

x と y の両方を一度に配列で取得するゲッターを提供することを理解しています。ここでのトリックは何ですか?

ここで必要なのは、コードの重複を避けるためのコンストラクター呼び出しの連鎖です。理想的には、次のようなものが必要です。

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(p.x, p.y);
}

の呼び出しの間に値が変更された可能性があるという条件があるからp.get()です。

そのため、SafePoint から値を取得し、別のコンストラクターにチェーンします。そのため、プライベート コンストラクター キャプチャ イディオムを使用して、プライベート コンストラクターで値をキャプチャし、「実際の」コンストラクターにチェーンします。

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

また、

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

クラスの外では意味がありません。2 次元の点には、配列が示唆するような任意の値ではなく、2 つの値があります。配列の長さのチェックも、そうでないことのチェックもありませんnull。これはクラス内でのみ使用され、呼び出し元は配列から 2 つの値を呼び出しても安全であることを認識しています。

于 2012-08-19T18:51:37.033 に答える
2

SafePoint を使用する目的は、常に x と y の一貫したビューを提供することです。

たとえば、SafePoint が (1,1) であるとします。そして、あるスレッドがこの SafePoint を読み取ろうとしているときに、別のスレッドがそれを (2,2) に変更しようとしています。セーフ ポイントがスレッド セーフでない場合、セーフ ポイントが (1,2) (または (2,1)) であるビューが表示され、一貫性がありません。

スレッドセーフな一貫したビューを提供するための最初のステップは、x と y への独立したアクセスを提供しないことです。ただし、両方に同時にアクセスする方法を提供する必要があります。同様の契約が修飾子メソッドにも適用されます。

この時点で、SafePoint 内にコピー コンストラクターが実装されていない場合は、完全に実装されています。ただし、実装する場合は注意が必要です。コンストラクターは同期できません。次のような実装では、px と py が個別にアクセスされるため、矛盾した状態が発生します。

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

しかし、以下はスレッドセーフを壊しません。

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

コードを再利用するために、int 配列を受け入れるプライベート コンストラクターが実装され、this(x, y) に委譲されます。int 配列コンストラクターは公開できますが、実際には this(x, y) と同様になります。

于 2012-08-19T19:56:34.287 に答える
0

コンストラクターは、このクラスの外部で使用することは想定されていません。クライアントは、配列を作成してこのコンストラクターに渡すことはできません。

他のすべてのパブリック コンストラクターは、SafePoint の get メソッドが呼び出されることを意味します。

プライベート コンストラクターを使用すると、おそらくスレッドの安全でない方法で独自のコンストラクターを構築できます (つまり、x、y を個別に取得し、配列を構築して渡すことにより)。

于 2012-08-19T18:51:56.693 に答える
0

私たちの要件は次のとおりです: 以下のようなコピー コンストラクターが必要です (同時に、クラスがまだスレッド セーフであることを保証します)。

public SafePoint(SafePoint p){
    // clones 'p' passed a parameter and return a new SafePoint object.
}

それではコピーコンストラクタを作ってみましょう。

アプローチ 1:

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

上記のアプローチの問題は、クラスが NOT THREAD SAFE になることです。

どのように ?

コンストラクターは同期されていないため、2 つのスレッドが同じオブジェクトに対して同時に動作する可能性があります (1 つのスレッドがコピー コンストラクターを使用してこのオブジェクトを複製し、別のスレッドがオブジェクトのセッター メソッドを呼び出す可能性があります)。これが発生した場合、setter メソッドを呼び出したスレッドがフィールドを更新した可能性がありx(まだフィールドを更新していないためy)、オブジェクトが一貫性のない状態でレンダリングされます。この時点で、(オブジェクトのクローンを作成していた) 他のスレッドが実行された場合 (コンストラクターが固有ロックによって同期されていないため、実行可能である場合)、コピー コンストラクターthis(p.x, p.y)p.x新しい値になりますが、p.y古い値のままです。

したがって、コンストラクターが同期されていないため、私たちのアプローチはスレッドセーフではありません。

アプローチ2:(アプローチ1をスレッドセーフにしようとしています)

public SafePoint(SafePoint p){
    int[] temp = p.get();
    this(temp[0], temp[1]);
}

これはp.get()、組み込みロックによって同期されるため、スレッドセーフです。したがって、p.get()実行中、ゲッターとセッターの両方が同じ組み込みロックによって保護されているため、他のスレッドはセッターを実行できませんでした。

しかし残念ながら、コンパイラはこれを許可しませんthis(p.x, p.y)

これにより、最終的なアプローチに進みます。

アプローチ 3: (アプローチ 2 のコンパイルの問題を解決する)

public SafePoint(SafePoint p){
    this(p.get());
}

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

このアプローチにより、クラスがスレッド セーフであり、コピー コンストラクターがあることが保証されます。

残っている最後の質問は、なぜ 2 番目のコンストラクターが private なのかということです。これは単に、このコンストラクターを内部的な目的で作成し、クライアントがこのメソッドを呼び出して SafePoint オブジェクトを作成することを望まないためです。

于 2018-07-28T19:52:31.177 に答える