12

1 つのフィールドのみを含む次のクラスがありますi。このフィールドへのアクセスは、オブジェクト ("this") のロックによって保護されています。equals() を実装するときは、このインスタンス (a) と他のインスタンス (b) をロックする必要があります。スレッド 1 が a.equals(b) を呼び出し、同時にスレッド 2 が b.equals(a) を呼び出すと、2 つの実装でロックの順序が逆になり、デッドロックが発生する可能性があります。

同期されたフィールドを持つクラスに equals() を実装するにはどうすればよいですか?

public class Sync {
    // @GuardedBy("this")
    private int i = 0;
    public synchronized int getI() {return i;}
    public synchronized void setI(int i) {this.i = i;}

    public int hashCode() {
        final int prime = 31;
        int result = 1;
        synchronized (this) {
            result = prime * result + i;
        }
        return result;
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Sync other = (Sync) obj;
        synchronized (this) {
            synchronized (other) {
                // May deadlock if "other" calls 
                // equals() on "this" at the same 
                // time 
                if (i != other.i)
                    return false;
            }
        }
        return true;
    }
}
4

14 に答える 14

10

オブジェクト内で同期しようとするequalshashCode、正しく動作しません。を使用してオブジェクトがどの「バケット」にあるかを検出し、 を使用しHashMapてバケット内のすべてのオブジェクトを順次検索する場合を考えてみましょう。hashCodeequals

の結果を変更する方法でオブジェクトを変更することが許可されている場合、hashCodeまたはを呼び出すequalsシナリオになる可能性があります。ロックを取得し、ハッシュを取得して、ロックを再度解放します。 次に、使用する「バケット」の計算に進みます。しかし、ロックを取得する前に、他の誰かがロックを取得してオブジェクトを変更し、以前の の値と一致しなくなります。これは壊滅的な結果につながります。HashMaphashCodeHashMapHashMapequalshashCode

およびhashCodeequals メソッドは多くの場所で使用され、Java コレクション API の中核です。これらのメソッドへの同期アクセスを必要としないアプリケーション構造を再考する価値があるかもしれません。または、少なくともオブジェクト自体を同期しないでください。

于 2009-10-28T10:41:13.753 に答える
7

なぜ同期するのですか?比較中にそれらが変更されるかどうかが重要であり、同等性に応じてコードが実行される直前に変更されるかどうかは問題ではないユースケースは何ですか。(つまり、等号に依存するコードがある場合、このコードの前または実行中に値が不等になるとどうなるか)

ロックする必要がある場所を確認するには、より大きなプロセスを確認する必要があると思います。

于 2009-10-28T10:35:23.620 に答える
6

同期が終了した後に結果がtrueであることが保証されていない場合、equals()を同期するポイントはどこにありますか。

if (o1.equals(o2)) {
  // is o1 still equal to o2?
}

したがって、出力を変更せずに、内部のgetI()への呼び出しを次々に同期することができます-それは単純でもはや有効ではありません。

ブロック全体を常に同期する必要があります。

synchronized(o1) {
  synchronized(o2) {
    if (o1.equals(o2)) {
      // is o1 still equal to o2?
    }
  }
}

確かに、あなたはまだ同じ問題に直面するでしょうが、少なくとも正しいポイントでの同期;)

于 2009-10-28T10:48:05.757 に答える
3

十分に言われている場合、hashCode()、equals()、またはcompareTo()に使用するフィールドは不変で、できればfinalにする必要があります。この場合、それらを同期する必要はありません。

hashCode() を実装する唯一の理由は、オブジェクトをハッシュ コレクションに追加できるようにするためであり、そのようなコレクションに追加されたオブジェクトの hashCode() を有効に変更することはできません。

于 2009-10-28T20:18:16.843 に答える
2

変更可能なオブジェクトでコンテンツベースの「equals」と「hashCode」を定義しようとしています。これは不可能であるだけでなく、意味がありません。によると

http://java.sun.com/javase/6/docs/api/java/lang/Object.html

「equals」と「hashCode」の両方が一貫している必要があります。同じオブジェクトに対する連続する呼び出しに対して同じ値を返します。定義による可変性はそれを防ぎます。これは単なる理論ではありません。他の多くのクラス (コレクションなど) は、equals/hashCode の正しいセマンティクスを実装するオブジェクトに依存しています。

同期の問題は、ここではニシンです。根本的な問題 (可変性) を解決すると、同期する必要がなくなります。可変性の問題を解決しなければ、いくら同期しても役に立ちません。

于 2009-11-08T14:00:54.800 に答える
1

(ここでは、ラップされた整数だけでなく、一般的なケースに関心があると思います。)

set2つのスレッドが...メソッドを任意の順序で呼び出すのを防ぐことはできません。したがって、あるスレッドが...のtrue呼び出しから(有効な)を取得した場合でも、その結果は、オブジェクトの1つで...を呼び出す別のスレッドによってすぐに無効になる可能性があります。IOWの結果は、比較の瞬間に値が等しいことを意味するだけです。.equals()set

したがって、同期すると、比較を実行しようとしているときにラップされた値が不整合な状態になるのを防ぐことができます(たとえばint、ラップされた2つのサイズの半分がlong連続して更新されます)。各値をコピーして(つまり、重複せずに独立して同期して)競合状態を回避し、コピーを比較することができます。

于 2009-10-28T11:56:35.383 に答える
1

同期が厳密に必要かどうかを確実に知る唯一の方法は、プログラム全体を状況について分析することです。探す必要があるものが 2 つあります。1 つのスレッドがオブジェクトを変更しているときに別のスレッドが equals を呼び出している状況、および呼び出しているスレッドequalsが の古い値を参照する可能性がある状況i

両方thisotherオブジェクトを同時にロックすると、実際にデッドロックが発生する危険があります。しかし、これを行う必要があるかどうかは疑問です。代わりに、次のように実装する必要があると思いますequals(Object)

public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Sync other = (Sync) obj;
    return this.getI() == other.getI();
}

これは、2 つのオブジェクトが同時に同じ値を持つことを保証するものではありませんがi、実質的な違いはほとんどありません。結局のところ、その保証があったとしても、equals呼び出しが返されるまでに 2 つのオブジェクトが等しくない可能性があるという問題に対処する必要があります。(これは@sのポイントです!)

さらに、これはデッドロックのリスクを完全に排除するものではありません。equals2 つのオブジェクトのいずれかをロックしているときにスレッドが呼び出す場合を考えてみましょう。例えば

// In same class as above ...
public synchronized void frobbitt(Object other) {
    if (this.equals(other)) {
        ...
    }
}

2 つのスレッドがそれぞれ and を呼び出すa.frobbitt(b)b.frobbitt(a)、デッドロックのリスクがあります。

(ただし、 を呼び出すか、 beをgetI()宣言する必要があります。そうしないと、別のスレッドによって最近更新された場合に、 の古い値が表示される可能性があります。)ivolatileequals()i

そうは言ってもequals、コンポーネントの値が変更される可能性があるオブジェクトの値ベースのメソッドについては、かなり懸念されることがあります。たとえば、これにより多くのコレクション型が壊れます。これをマルチスレッドと組み合わせると、コードが本当に正しいかどうかを判断するのが非常に難しくなります。メソッドが最初に呼び出された後に変化する可能性のある状態に依存しないように、 equalsandメソッドを変更した方がよいと思わずにはいられません。hashcode

于 2009-10-28T11:42:19.040 に答える
1
于 2009-10-28T10:40:23.270 に答える
0

他の人が述べているように、等しいチェック中に物事が変化した場合、(正しい同期があっても)すでにクレイジーな動作の可能性があります。したがって、本当に心配する必要があるのは可視性だけです(equals呼び出しの「前に発生する」変更が表示されることを確認する必要があります)。したがって、「スナップショット」イコールを実行できます。これは、「前に起こる」関係の観点から正しく、ロックの順序の問題に悩まされることはありません。

public boolean equals(Object o) {
  // ... standard boilerplate here ...

  // take a "snapshot" (acquire and release each lock in turn)
  int myI = getI();
  int otherI = ((Sync)o).getI();

  // and compare (no locks held at this point)
  return myI == otherI;
}
于 2011-09-12T00:54:13.487 に答える
0

Jason Day が指摘するように、整数の比較はすでにアトミックであるため、ここでの同期は不要です。しかし、単純化された例を作成しているだけで、実際にはより複雑なオブジェクトを考えている場合:

あなたの質問に対する直接的な答えは、常に一貫した順序で項目を比較するようにすることです。一貫性がある限り、その順序は問題ではありません。このような場合、System.identifyHashCode は次のような順序を提供します。

public boolean equals(Object o)
{
  if (this==o)
    return true;
  if (o==null || !o instanceof Sync)
    return false;
  Sync so=(Sync) o;
  if (System.identityHashCode(this)<System.identityHashCode(o))
  {
    synchronized (this)
    {
      synchronized (o)
      {
         return equalsHelper(o);
      }
    }
  }
  else
  {
    synchronized (o)
    {
      synchronized (this)
      {
        return equalsHelper(o);
      }
    }
  }
}

次に、equalsHelper を非公開として宣言し、実際の比較作業を実行させます。

(しかし、うわー、それはそのような些細な問題のための多くのコードです.)

これが機能するには、オブジェクトの状態を変更できる関数はすべて同期されていると宣言する必要があることに注意してください。

もう 1 つのオプションは、いずれかのオブジェクトではなく Sync.class で同期し、Sync.class のセッターも同期することです。これにより、単一のミューテックスですべてがロックされ、問題全体が回避されます。もちろん、何をしているかによっては、一部のスレッドで望ましくないブロックが発生する可能性があります。プログラムの内容に照らして、その意味を考えなければなりません。

取り組んでいるプロジェクトでこれが実際の問題である場合、考慮すべき重大な代替案は、オブジェクトを不変にすることです。String と StringBuilder について考えてみてください。これらのいずれかを作成するために必要な作業を実行できる SyncBuilder オブジェクトを作成し、コンストラクタによって状態が設定され、決して変更できない Sync オブジェクトを作成できます。SyncBuilder を受け取り、その状態を一致するように設定するか、SyncBuilder.toSync メソッドを持つコンストラクターを作成します。いずれにせよ、すべての構築を SyncBuilder で行い、それを Sync に変換すると、不変性が保証されるので、同期をいじる必要はまったくありません。

于 2009-10-28T13:21:05.517 に答える
0

との正しい実装はequals()hashCode()データ構造のハッシュなどのさまざまなことで必要とされるため、実際のオプションはありません。別の観点から言えば、equals()hashCode()は単なるメソッドであり、同期に関する要件は他のメソッドと同じです。デッドロックの問題はまだありますが、それがequals()原因であるという事実に固有のものではありません。

于 2009-10-28T10:35:08.957 に答える
0

hashCode() と equals() (呼び出された場合) の呼び出しの間でオブジェクトが変更されないようにする必要があります。次に、オブジェクトがハッシュマップにある間、オブジェクトが変更されないことを保証する必要があります (hashCode と equals が関係する範囲まで)。オブジェクトを変更するには、まずオブジェクトを削除し、次に変更して元に戻す必要があります。

于 2011-09-12T00:26:19.730 に答える
0

同期を使用しないでください。変更不可能な Bean について考えてみてください。

于 2009-10-28T23:58:30.220 に答える
-2

変数への読み取りと書き込みintはすでにアトミックであるため、getter と setter を同期する必要はありません ( http://java.sun.com/docs/books/tutorial/essential/concurrency/atomic.htmlを参照)。

同様に、ここで同期する必要はありませんequals。比較中に別のスレッドが値の 1 つを変更するのを防ぐことはできますがi、そのスレッドはメソッドが完了するまで単にブロックされ、equalsその後すぐに値が変更されます。

于 2009-10-28T12:07:19.077 に答える