17


14

Javaの同時実行性:多くのライター、1人のリーダー

ソフトウェアで統計を収集する必要があり、それを迅速かつ正確にしようとしていますが、これは簡単ではありません(私にとっては!)

最初に、これまでの2つのクラス、StatsServiceとStatsHarvesterを使用したコード

public class StatsService
{
private Map   stats   = new HashMap(1000);

public void notify ( String key )
{
    Long value = 1l;
    synchronized (stats)
    {
        if (stats.containsKey(key))
        {
            value = stats.get(key) + 1;
        }
        stats.put(key, value);
    }
}

public Map getStats ( )
{
    Map copy;
    synchronized (stats)
    {
        copy = new HashMap(stats);
        stats.clear();
    }
    return copy;
}
}

これは私の2番目のクラスで、時々統計を収集してデータベースに書き込むハーベスターです。

public class StatsHarvester implements Runnable
{
private StatsService    statsService;
private Thread          t;

public void init ( )
{
    t = new Thread(this);
    t.start();
}

public synchronized void run ( )
{
    while (true)
    {
        try
        {
            wait(5 * 60 * 1000); // 5 minutes
            collectAndSave();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

private void collectAndSave ( )
{
    Map stats = statsService.getStats();
    // do something like:
    // saveRecords(stats);
}
}

実行時には、約30回の同時実行スレッドがあり、それぞれが約100回「notify(key)」を呼び出します。 1つのStatsHarvesterのみが `statsService.getStats()`を呼び出しています

だから私は多くの作家と一人の読者がいます。 正確な統計情報があればいいのですが、高い並行性でいくつかのレコードが失われてもかまいません。

リーダーは、5分ごと、または合理的な範囲で実行する必要があります。

書き込みはできるだけ速くする必要があります。 読み取りは高速になりますが、5分ごとに約300ミリ秒間ロックされる場合は問題ありません。

私は多くのドキュメント(実際にはJavaの並行性、効果的なJavaなど)を読みましたが、それを正しくするためにはあなたのアドバイスが必要だと強く感じています。

貴重な助けを得るのに十分な問題を明確かつ簡潔に述べたいと思います。

'' '' '

EDIT

詳細かつ有益な回答をありがとう。 予想通り、それを行う方法は複数あります。

私はあなたの提案のほとんど(私が理解した)をテストし、さらに参照するためにGoogleプロジェクトにテストプロジェクトをアップロードしました(Mavenプロジェクト)

StatsServiceのさまざまな実装をテストしました

  • HashMapStatsService(HMSS)

  • ConcurrentHashMapStatsService(CHMSS)

  • LinkedQueueStatsService(LQSS)

  • GoogleStatsService(GSS)

  • ExecutorConcurrentHashMapStatsService(ECHMSS)

  • ExecutorHashMapStatsService(EHMSS)

そして、それぞれxの数のスレッドでnotify yを呼び出してテストしました。結果はmsです

         10,100   10,1000  10,5000  50,100   50,1000  50,5000  100,100  100,1000 100,5000
GSS       1        5        17       7        21       117      7        37       254       Summe: 466
ECHMSS    1        6        21       5        32       132      8        54       249       Summe: 508
HMSS      1        8        45       8        52       233      11       103      449       Summe: 910
EHMSS     1        5        24       7        31       113      8        67       235       Summe: 491
CHMSS     1        2        9        3        11       40       7        26       72        Summe: 171
LQSS      0        3        11       3        16       56       6        27       144       Summe: 266

現時点では、ConcurrentHashMapを使用すると思います。優れたパフォーマンスを提供する一方で、非常に理解しやすいからです。

ご協力ありがとうございます ジャニング

9 Answer


16


ジャックが逃げていたので、ConcurrentHashMapとAtomicLongを含むjava.util.concurrentライブラリを使用できます。 AtomicLongを他にない場合は、値を増やすことができます。 AtomicLongはスレッドセーフであるため、同時実行の問題を心配することなく変数をインクリメントできます。

public void notify(String key) {
    AtomicLong value = stats.get(key);
    if (value == null) {
        value = stats.putIfAbsent(key, new AtomicLong(1));
    }
    if (value != null) {
        value.incrementAndGet();
    }
}

これは高速でスレッドセーフでなければなりません

編集:わずか2回のルックアップのみが行われるように、リファクタリングしました。


7


「java.util.concurrent.ConcurrentHashMap」を使用しないのはなぜですか? 内部的にすべてを処理して、マップ上の不要なロックを回避し、多くの作業を節約します。getおよびputの同期を気にする必要はありません。

ドキュメントから:

_ 検索の完全な同時実行性と更新の調整可能な予想同時実行性をサポートするハッシュテーブル。 このクラスはHashtableと同じ機能仕様に従い、Hashtableの各メソッドに対応するメソッドのバージョンを含みます。 ただし、すべての操作はスレッドセーフですが、取得操作にはロックは必要ありません。また、すべてのアクセスを妨げる方法でテーブル全体をロックすることはサポートされていません。 _

_concurrency level_を指定できます:

_ 更新操作間で許可される同時実行性は、オプションのconcurrencyLevelコンストラクター引数(デフォルト16)によってガイドされます。この引数は、内部サイジングのヒントとして使用されます。 示された数の同時更新を競合なしで許可しようとするために、表は内部的にパーティション化されています。 ハッシュテーブルへの配置は基本的にランダムなので、実際の同時実行性は異なります。 理想的には、テーブルを同時に変更するスレッドの数に対応する値を選択する必要があります。 必要以上に高い値を使用するとスペースと時間が無駄になり、低い値を使用するとスレッドの競合が発生する可能性があります。 しかし、一桁の範囲内で過大評価したり過小評価したりしても、それほど大きな影響はありません。 1つのスレッドのみが変更され、他のすべてのスレッドが読み取りのみを行うことがわかっている場合、1の値が適切です。 また、これまたは他の種類のハッシュテーブルのサイズ変更は比較的遅い操作であるため、可能な場合は、コンストラクターで予想されるテーブルサイズの推定値を提供することをお勧めします。 _

コメントで示唆されているように、http://java.sun.com/javase/6/docs/api/java/util/concurrent/ConcurrentHashMap.html [ConcurrentHashMap]のドキュメントを注意深く読んでください。特に、アトミック操作またはアトミック操作ではない場合。

アトミック性を保証するには、どの操作がアトミックかを考慮する必要があります。`ConcurrentMap`インターフェースから、次のことがわかります。

V putIfAbsent(K key, V value)
V replace(K key, V value)
boolean replace(K key,V oldValue, V newValue)
boolean remove(Object key, Object value)

安全に使用できます。


5


Javaのutil.concurrentライブラリをご覧になることをお勧めします。 このソリューションをもっときれいに実装できると思います。 ここに地図は必要ないと思います。 ConcurrentLinkedQueueを使用してこれを実装することをお勧めします。 各「プロデューサー」は、他の人を心配することなく、このキューに自由に書き込むことができます。 統計用のデータとともにオブジェクトをキューに入れることができます。

ハーベスタは、キューを継続的に消費して、データを引き出して処理することができます。 その後、必要に応じて保存できます。


4


クリス・デイルの答えは良いアプローチのようです。

別の選択肢は、同時の「マルチセット」を使用することです。 Googleコレクションライブラリにあります。 これは次のように使用できます。

private Multiset stats = ConcurrentHashMultiset.create();

public void notify ( String key )
{
    stats.add(key, 1);
}

sourceを見ると、これは `ConcurrentHashMap`を使用して実装されていますそして、「putIfAbsent」と3引数バージョンの「replace」を使用して、同時変更を検出し、再試行します。


3


この問題に対する別のアプローチは、スレッドの制限を介して(簡単な)スレッドセーフティを活用することです。 基本的に、読み取りと書き込みの両方を処理する単一のバックグラウンドスレッドを作成します。 スケーラビリティとシンプルさの点で非常に優れた特性があります。

これは、すべてのスレッドがデータを直接更新しようとする代わりに、バックグラウンドスレッドが処理する「更新」タスクを生成するという考え方です。 更新の処理に多少の遅延が許容されると仮定すると、同じスレッドで読み取りタスクを実行することもできます。

この設計は、スレッドがデータを更新するためにロックを奪い合う必要がなく、マップが単一のスレッドに限定されているため、単純なHashMapを使用してget / putなどを実行できるため、非常に便利です。 実装に関しては、シングルスレッドエグゼキュータを作成し、オプションの「collectAndSave」操作を実行する書き込みタスクを送信することを意味します。

コードのスケッチは次のようになります。

public class StatsService {
    private ExecutorService executor = Executors.newSingleThreadExecutor();
    private final Map stats = new HashMap();

    public void notify(final String key) {
        Runnable r = new Runnable() {
            public void run() {
                Long value = stats.get(key);
                if (value == null) {
                    value = 1L;
                } else {
                    value++;
                }
                stats.put(key, value);
                // do the optional collectAndSave periodically
                if (timeToDoCollectAndSave()) {
                    collectAndSave();
                }
            }
        };
        executor.execute(r);
    }
}

executorに関連付けられたBlockingQueueがあり、StatsServiceのタスクを生成する各スレッドはBlockingQueueを使用します。 重要な点は次のとおりです。*この操作のロック期間は、元のコードのロック期間よりもはるかに短くする必要があります。したがって、競合はずっと少なくなります。 全体として、スループットとレイテンシが大幅に向上するはずです。

もう1つの利点は、1つのスレッドだけがマップの読み取りと書き込みを行うため、プレーンHashMapとプリミティブlong型を使用できることです(ConcurrentHashMapまたはアトミック型は含まれません)。 これにより、実際に大量に処理するコードも簡素化されます。

それが役に立てば幸い。


1


http://java.sun.com/javase/6/docs/api/java/util/concurrent/ScheduledThreadPoolExecutor.html [ScheduledThreadPoolExecutor]を調べましたか? これを使用して、@ Chris Dailが言及した「ConcurrentLinkedQueue」などの並行コレクションにすべて書き込むことができるライターをスケジュールできます。 必要に応じて、キューから読み取るジョブを個別にスケジュールできます。JavaSDKは、同時ロックに関するほとんどすべての問題を処理する必要があり、手動でロックする必要はありません。


0


ハーベスティングの部分を無視して記述に集中すると、プログラムの主なボトルネックは、統計が非常に粗いレベルの粒度でロックされることです。 2つのスレッドが異なるキーを更新する場合は、待機する必要があります。

キーのセットを事前に知っており、更新スレッドが到着するまでにキーが存在することが保証されるようにマップを事前初期化できる場合、マップ全体ではなくアキュムレータ変数をロックするか、またはスレッドセーフアキュムレータオブジェクト。

これを自分で実装する代わりに、並行性のために特別に設計されたマップ実装があり、これはよりきめ細かいロックを行います。

ただし、1つ注意点があります。これは、すべてのアキュムレーターのロックをほぼ同時に取得する必要があるためです。 既存の並行処理に適したマップを使用する場合、スナップショットを取得するための構成が存在する場合があります。


0


ReentranReadWriteLockを使用して両方のメソッドを実装する別の代替手段。 この実装は、カウンターをクリアする必要がある場合、getStatsメソッドでの競合状態から保護します。 また、可変のAtomicLongをgetStatsから削除し、不変のLongを使用します。

public class StatsService {

    private final Map stats = new HashMap(1000);
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    public void  notify(final String key) {
        r.lock();
        AtomicLong count = stats.get(key);
        if (count == null) {
            r.unlock();
            w.lock();
            count = stats.get(key);
            if(count == null) {
                count = new AtomicLong();
                stats.put(key, count);
            }
            r.lock();
            w.unlock();
        }
        count.incrementAndGet();
        r.unlock();
    }

    public Map getStats() {
        w.lock();

        Map copy = new HashMap();
        for(Entry entry : stats.entrySet() ){
                copy.put(entry.getKey(), entry.getValue().longValue());
        }
        stats.clear();
        w.unlock();

        return copy;
    }
}

これがお役に立てば幸いです、コメントは大歓迎です!


0


以下は、測定対象のスレッドのパフォーマンスへの影響を最小限に抑えて実行する方法です。 これは、パフォーマンスカウント用の特別なハードウェアレジスタに頼ることなく、Javaで可能な最速のソリューションです。

各スレッドに、他のスレッドとは独立して、つまり同期なしで、いくつかの統計オブジェクトに統計を出力させます。 カウントを含むフィールドを揮発性にし、メモリフェンスにします。

class Stats
{
   public volatile long count;
}

class SomeRunnable implements Runnable
{
   public void run()
   {
     doStuff();
     stats.count++;
   }
}

すべてのStatsオブジェクトへの参照を保持する別のスレッドがあり、定期的にそれらすべてを回って、すべてのスレッドでカウントを合計します。

public long accumulateStats()
{
   long count = previousCount;

   for (Stats stat : allStats)
   {
       count += stat.count;
   }

   long resultDelta = count - previousCount;
   previousCount = count;

   return resultDelta;
}

このGathererスレッドには、sleep()(またはその他のスロットル)を追加する必要もあります。 たとえば、カウント/秒をコンソールに定期的に出力して、アプリケーションのパフォーマンスの「ライブ」ビューを提供できます。

これにより、可能な限り同期のオーバーヘッドが回避されます。

考慮すべきもう1つのトリックは、Statsオブジェクトを128(SandyBridge以降では256バイト)にパディングし、異なるキャッシュラインで異なるスレッドカウントを保持することです。そうしないと、CPUでキャッシュの競合が発生します。

1つのスレッドだけが読み取りと1つの書き込みを行う場合、ロックやアトミックは必要ありません。揮発性で十分です。 統計リーダースレッドが測定対象のスレッドのCPUキャッシュラインと相互作用する場合、スレッドの競合がまだあります。 これは避けることはできませんが、実行中のスレッドへの影響を最小限に抑える方法です。たぶん1秒に1回以下の統計を読んでください。