0

Java でのマルチスレッドについては、まだかなり不安定です。ここで説明することは、私のアプリケーションの核心であり、これを正しく理解する必要があります。ソリューションは迅速に機能する必要があり、実質的に安全である必要があります。これは機能しますか?提案/批判/代替ソリューションを歓迎します。


アプリケーション内で使用されるオブジェクトは、生成するのにいくらかコストがかかりますが、めったに変更されないため、*.temp ファイルにキャッシュしています。あるスレッドが特定のオブジェクトをキャッシュから取得しようとしているときに、別のスレッドがそのオブジェクトを更新しようとしている可能性があります。検索と保存のキャッシュ操作は、CacheService 実装内にカプセル化されます。

次のシナリオを検討してください。

Thread 1: retrieve cache for objectId "page_1".
Thread 2: update cache for objectId "page_1".
Thread 3: retrieve cache for objectId "page_2".
Thread 4: retrieve cache for objectId "page_3".
Thread 5: retrieve cache for objectId "page_4".

注: スレッド 1 は古いオブジェクトを取得しているように見えます。これは、スレッド 2 が古いオブジェクトの新しいコピーを持っているためです。これはまったく問題ないので、スレッド 2 を優先するロジックは必要ありません。

サービスで取得/保存メソッドを同期すると、スレッド 3、4、および 5 の動作が不必要に遅くなります。複数の取得操作は常に有効ですが、更新操作が呼び出されることはめったにありません。これが、メソッドの同期を避けたい理由です。

スレッド 1 と 2 に排他的に共通するオブジェクトで同期する必要があることを収集します。これは、ロック オブジェクト レジストリを意味します。ここでは、明らかな選択肢は Hashtable ですが、Hashtable の操作は同期されているため、HashMap を試しています。マップには、同期用のロック オブジェクトとして使用される文字列オブジェクトが格納されます。キー/値は、キャッシュされるオブジェクトの ID になります。したがって、オブジェクト「page_1」のキーは「page_1」になり、ロック オブジェクトは「page_1」の値を持つ文字列になります。

レジストリが正しい場合は、さらに、エントリが多すぎないように保護したいと考えています。その理由については詳しく説明しません。レジストリが定義された制限を超えて成長した場合、0 要素で再初期化する必要があると仮定しましょう。これは、同期されていない HashMap では少しリスクがありますが、このフラッディングは、通常のアプリケーション操作の外にあるものです。それは非常にまれな出来事であり、うまくいけば決して起こらないはずです. でも可能性があるからこそ、自分を守りたい。

@Service
public class CacheServiceImpl implements CacheService {
    private static ConcurrentHashMap<String, String> objectLockRegistry=new ConcurrentHashMap<>();

public Object getObject(String objectId) {
    String objectLock=getObjectLock(objectId);
    if(objectLock!=null) {
        synchronized(objectLock) {
            // read object from objectInputStream
    }
}

public boolean storeObject(String objectId, Object object) {
    String objectLock=getObjectLock(objectId);

    synchronized(objectLock) {
        // write object to objectOutputStream
    }
}

private String getObjectLock(String objectId) {
    int objectLockRegistryMaxSize=100_000;

    // reinitialize registry if necessary
    if(objectLockRegistry.size()>objectLockRegistryMaxSize) {
        // hoping to never reach this point but it is not impossible to get here
        synchronized(objectLockRegistry) {
            if(objectLockRegistry.size()>objectLockRegistryMaxSize) {
                objectLockRegistry.clear();
            }
        }
    }

    // add lock to registry if necessary
    objectLockRegistry.putIfAbsent(objectId, new String(objectId));

    String objectLock=objectLockRegistry.get(objectId);
    return objectLock;
}
4

6 に答える 6

3

スキームの複雑さについてはすでに説明しました。それはバグを見つけるのが難しいことにつながります。たとえば、非最終変数をロックするだけでなく、それらをロックとして使用する同期ブロックの途中で変更することもできます。マルチスレッドについて推論するのは非常に困難です。この種のコードでは、ほとんど不可能です。

    synchronized(objectLockRegistry) {
        if(objectLockRegistry.size() > objectLockRegistryMaxSize) {
            objectLockRegistry = new HashMap<>(); //brrrrrr...
        }
    }

特に、特定の文字列をロックするための2つの同時呼び出しは、実際には同じ文字列の2つの異なるインスタンスを返す可能性があり、それぞれがハッシュマップの異なるインスタンスに格納され(インターンされていない限り)、ロックされません。同じモニター。

既存のライブラリを使用するか、それをずっと単純にしておく必要があります。

于 2012-11-25T22:18:23.520 に答える
3

ディスクから読み取る場合、ロックの競合はパフォーマンスの問題にはなりません。

両方のスレッドでキャッシュ全体のロックを取得し、読み取りを行い、値が欠落している場合はロックを解放し、ディスクから読み取り、ロックを取得し、値がまだ欠落している場合は書き込み、そうでない場合は値を返すことができますそれが今ある。

それに関する唯一の問題は、同時読み取りがディスクを破壊することです...しかし、OS キャッシュは熱くなるので、ディスクを過度に破壊するべきではありません。

それが問題になる場合は、キャッシュを のFuture<V>代わりにを保持するように切り替えます<V>

get メソッドは次のようになります。

public V get(K key) {
    Future<V> future;
    synchronized(this) {
        future = backingCache.get(key);
        if (future == null) {
            future = executorService.submit(new LoadFromDisk(key));
            backingCache.put(key, future);
        }
    }
    return future.get();
}

はい、それはグローバルロックです...しかし、ディスクから読み取っていて、パフォーマンスのボトルネックが証明されるまで最適化しないでください...

おー。最初の最適化。マップを a に置き換えてConcurrentHashMap使用するputIfAbsentと、ロックがまったくなくなります。(ただし、これが問題であることがわかっている場合にのみ行ってください)

于 2012-11-25T22:03:44.353 に答える
1

質問に「最適化」、「並行」というキーワードが含まれていて、ソリューションに複雑なロックスキームが含まれている場合は、間違っています。この種のベンチャーで成功することは可能ですが、オッズはあなたに対して積み重なっています。デッドロック、ライブロック、キャッシュの非一貫性など、奇妙な同時実行性のバグを診断する準備をします。サンプルコードで複数の危険なプラクティスを見つけることができます。

並行性の神でなくても安全で効果的な並行アルゴリズムを作成するほとんど唯一の方法は、事前に作成された並行クラスの1つを取得し、それらをニーズに適合させることです。非常に説得力のある理由がない限り、それを行うのは非常に困難です。

あなたは見てみるかもしれませんConcurrentMap。あなたも好きかもしれませんCacheBuilder

于 2012-11-25T21:44:38.877 に答える
1

スレッドの使用と直接同期については、マルチスレッドと同時実行に関するほとんどのチュートリアルの冒頭で説明しています。ただし、実際の例の多くは、より高度なロックおよび同時実行スキームを必要とします。これらのスキームを自分で実装すると、煩雑でエラーが発生しやすくなります。車輪の再発明を何度も繰り返すことを防ぐために、Java同時実行ライブラリが作成されました。そこでは、あなたに大いに役立つであろう多くのクラスを見つけることができます。Javaの同時実行性とロックに関するチュートリアルをグーグルで試してみてください。

役立つ可能性のあるロックの例として、http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReadWriteLock.htmlを参照してください

于 2012-11-25T21:47:14.187 に答える
1

自分のキャッシュをロールバックするのではなく、GoogleのMapMakerを見てみましょう。このようなものは、未使用のエントリがガベージコレクションされるときに自動的に期限切れになるロックキャッシュを提供します。

ConcurrentMap<String,String> objectLockRegistry = new MapMaker()
    .softValues()
    .makeComputingMap(new Function<String,String> {
      public String apply(String s) {
        return new String(s);
      });

getObjectLockこれにより、実装全体が単純return objectLockRegistry.get(objectId)になります。マップは、安全な方法ですべての「まだ存在しない場合に作成する」ものを処理します。

于 2012-11-25T22:14:19.577 に答える
0

私はあなたに似たようなことをします: オブジェクト (new Object()) のマップを作成するだけです。
しかし、あなたとは異なり、私はTreeMap<String, Object> HashMapを使用するか、それをlockMapと呼びます。ロックするファイルごとに 1 つのエントリ。lockMap は、参加しているすべてのスレッドで公開されています。
特定のファイルへの読み取りと書き込みのたびに、マップからロックが取得されます。そして、そのロック オブジェクトで syncrobize(lock) を使用します。
lockMap が固定されておらず、その内容が変更されている場合は、マップへの読み取りと書き込みも同期する必要があります。(syncronized (this.lockMap) {....})
しかし、あなたの getObjectLock() は安全ではありません。それをすべてあなたのロックと同期してください。(ダブルチェックされたロックインは Java ではスレッドセーフではありません!) 推奨される本: Doug Lea、Java での並行プログラミング

于 2012-11-25T21:43:39.627 に答える