16

一連のジャーナルエントリを取得して合計を計算するアプリケーションを作成しています。

以下の方法は、メソッドを呼び出す複数のスレッドがある場合、スレッド/同時実行セーフaddToSum()です。各呼び出しが合計を適切に更新するようにしたいと考えています。

安全でない場合は、スレッドの安全性を確保するために何をしなければならないかを説明してください。

synchronizeget/put が必要ですか、それとももっと良い方法がありますか?

private ConcurrentHashMap<String, BigDecimal> sumByAccount;

public void addToSum(String account, BigDecimal amount){
    BigDecimal newSum = sumByAccount.get(account).add(amount);
    sumByAccount.put(account, newSum);
}

本当にありがとう!

アップデート:

答えてくれてありがとう、私はすでに上記のコードがスレッドセーフではないことを理解しています。

AtomicReferenceの代わりにを提案してくれた Vint に感謝しsynchronizeます。以前は整数の合計を保持しAtomicIntegerていましたが、BigDecimal にそのようなものがあるかどうか疑問に思っていました。

両者の長所と短所に関する決定的な結論はありますか?

4

4 に答える 4

18

他の人が提案したように同期を使用できますが、ブロックを最小限に抑えるソリューションが必要な場合はAtomicReference、 BigDecimal のストアとして試すことができます

ConcurrentHashMap<String,AtomicReference<BigDecimal>> map;

public void addToSum(String account, BigDecimal amount) {
    AtomicReference<BigDecimal> newSum = map.get(account);
    for (;;) {
       BigDecimal oldVal = newSum.get();
       if (newSum.compareAndSet(oldVal, oldVal.add(amount)))
            return;
    }
}

編集 - これについて詳しく説明します。

AtomicReference は、CASを使用して単一の参照をアトミックに割り当てます。ループはこう言っています。

AtomicReference に格納されている現在のフィールド == oldVal [値ではなくメモリ内の場所] の場合、AtomicReference に格納されているフィールドの値を に置き換えますoldVal.add(amount)。これで、newSum.get() を呼び出した for ループの後でいつでも、BigDecimal オブジェクトが追加されます。

2 つのスレッドが同じ AtomicReference に追加しようとする可能性があるため、ここでループを使用します。1 つのスレッドが成功し、別のスレッドが失敗する場合があります。その場合は、新しい追加値で再試行してください。

スレッドの競合が中程度の場合、これはより高速な実装になります。競合が多い場合は、使用したほうがよいでしょうsynchronized

于 2011-12-19T21:30:26.090 に答える
4

あなたの解決策はスレッドセーフではありません。その理由は、入力する操作が取得する操作とは別であるため、合計が失われる可能性があるためです(したがって、マップに入力する新しい値は、同時に追加される合計を見逃す可能性があります)。

やりたいことを行う最も安全な方法は、メソッドを同期することです。

于 2011-12-19T21:25:23.417 に答える
4

スレッド A と B の両方が(多かれ少なかれ) 同時に呼び出す可能性があるため、これは安全ではありません。つまり、次の順序で発生する可能性があります。sumByAccount.get(account)add(amount)

  • スレッド A はsumByAccount.get("accountX")、(たとえば) 10.0 を呼び出して取得します。
  • スレッド B はsumByAccount.get("accountX")、スレッド A が行ったのと同じ値 10.0 を呼び出して取得します。
  • スレッド A は、newSum(たとえば) 10.0 + 2.0 = 12.0 に設定します。
  • スレッド B は、newSum(たとえば) 10.0 + 5.0 = 15.0 に設定します。
  • スレッド A が を呼び出しますsumByAccount.put("accountX", 12.0)
  • スレッド B が を呼び出しsumByAccount.put("accountX", 15.0)、スレッド A が行ったことを上書きします。

これを修正する 1 つの方法は、メソッドを配置synchronizedするか、その内容をまたはaddToSumでラップすることです。上記の一連のイベントは、2 つのスレッドが同時に同じアカウントを更新している場合にのみ発生するため、別の方法として、ある種のオブジェクトに基づいて外部的に同期することもできます。プログラム ロジックの残りの部分を確認しないと、確信が持てません。synchronized(this)synchronized(sumByAccount)Account

于 2011-12-19T21:26:48.013 に答える
2

はい、同期する必要があります。そうしないと、2 つのスレッドがそれぞれ同じ値 (同じキーに対して) を取得する可能性があるためです。たとえば、A とスレッド 1 が B を追加し、スレッド 2 が C を追加して元に戻します。結果は A+B+C ではなく、A+B または A+C になります。

あなたがする必要があるのは、追加に共通する何かをロックすることです。get/put での同期は、そうしない限り役に立ちません。

synchronize {
    get
    add
    put
}

ただし、これを行うと、キーが異なる場合でも、スレッドが値を更新するのを防ぐことができます。アカウントで同期したい。ただし、デッドロックにつながる可能性があるため、文字列での同期は安全ではないようです (他に何が文字列をロックしているのかわかりません)。代わりにアカウント オブジェクトを作成し、それをロックに使用できますか?

于 2011-12-19T21:29:58.953 に答える