35

次のような、受け取ったパラメーターに基づいてメソッドを同期する方法を探しています。

public synchronized void doSomething(name){
//some code
}

次のようなパラメーターdoSomethingに基づいてメソッドを同期したい:name

スレッド 1: doSomething("a");

スレッド 2: doSomething("b");

スレッド 3: doSomething("c");

スレッド 4: doSomething("a");

スレッド 1、スレッド 2、およびスレッド 3 は同期せずにコードを実行しますが、スレッド 4 は同じ「a」値を持つため、スレッド 1 がコードを終了するまで待機します。

ありがとう

アップデート

チューダーの説明に基づいて、私は別の問題に直面していると思います: ここに新しいコードのサンプルがあります:

private HashMap locks=new HashMap();
public void doSomething(String name){
    locks.put(name,new Object());
    synchronized(locks.get(name)) {
        // ...
    }
    locks.remove(name);
}

ロック マップを作成しない理由は、name に任意の値を指定できるためです。

上記のサンプルに基づくと、HashMap はスレッドセーフではないため、複数のスレッドが同時にハッシュマップに値を追加または削除すると、問題が発生する可能性があります。

したがって、私の質問は、スレッドセーフなHashMapaを作成した場合ConcurrentHashMap、同期ブロックは他のスレッドが locks.get(name) にアクセスするのを停止しますか??

4

8 に答える 8

24

マップを使用して、文字列をロック オブジェクトに関連付けます。

Map<String, Object> locks = new HashMap<String, Object>();
locks.put("a", new Object());
locks.put("b", new Object());
// etc.

それから:

public void doSomething(String name){
    synchronized(locks.get(name)) {
        // ...
    }
}
于 2012-09-16T20:23:46.240 に答える
21

TL;DR:

Spring Framework のConcurrentReferenceHashMapを使用します。以下のコードを確認してください。


このスレッドは古いですが、それでも興味深いものです。したがって、Spring Framework での私のアプローチを共有したいと思います。

実装しようとしているのは、名前付きミューテックス/ロックと呼ばれます。Tudor's answerで示唆されているように、アイデアはMap、ロック名とロック オブジェクトを格納することです。コードは次のようになります (彼の回答からコピーします)。

Map<String, Object> locks = new HashMap<String, Object>();
locks.put("a", new Object());
locks.put("b", new Object());

ただし、このアプローチには 2 つの欠点があります。

  1. OPはすでに最初のものを指摘しました:locksハッシュマップへのアクセスを同期する方法は?
  2. 不要になったロックを削除するにはどうすればよいですか? そうしないと、locksハッシュ マップが拡大し続けます。

最初の問題は、 ConcurrentHashMapを使用して解決できます。2 番目の問題については、2 つのオプションがあります。ロックを手動でチェックしてマップから削除するか、ガベージ コレクターに使用されなくなったロックを認識させて、GC がそれらを削除するようにします。私は2番目の方法で行きます。

HashMap、またはを使用すると、ConcurrentHashMap強い参照が作成されます。上記のソリューションを実装するには、代わりに弱参照を使用する必要があります (強/弱参照とは何かを理解するには、この記事またはこの投稿を参照してください)。


そこで、Spring Framework のConcurrentReferenceHashMapを使用します。ドキュメントで説明されているように:

ConcurrentHashMapキーと値の両方にソフト参照または弱参照を使用する。

Collections.synchronizedMap(new WeakHashMap<K, Reference<V>>())このクラスは、同時アクセス時のパフォーマンスを向上させるための代替として使用でき ます。ConcurrentHashMapこの実装は、 null 値と null キーがサポートされていることを除いて、同じ設計上の制約に従います 。

これが私のコードです。は、キーのタイプであるすべてのMutexFactoryロックを管理します。<K>

@Component
public class MutexFactory<K> {

    private ConcurrentReferenceHashMap<K, Object> map;

    public MutexFactory() {
        this.map = new ConcurrentReferenceHashMap<>();
    }

    public Object getMutex(K key) {
        return this.map.compute(key, (k, v) -> v == null ? new Object() : v);
    }
}

使用法:

@Autowired
private MutexFactory<String> mutexFactory;

public void doSomething(String name){
    synchronized(mutexFactory.getMutex(name)) {
        // ...
    }
}

単体テスト (このテストでは、、などのいくつかのメソッドにawaitilityライブラリを使用します):await()atMost()until()

public class MutexFactoryTests {
    private final int THREAD_COUNT = 16;

    @Test
    public void singleKeyTest() {
        MutexFactory<String> mutexFactory = new MutexFactory<>();
        String id = UUID.randomUUID().toString();
        final int[] count = {0};

        IntStream.range(0, THREAD_COUNT)
                .parallel()
                .forEach(i -> {
                    synchronized (mutexFactory.getMutex(id)) {
                        count[0]++;
                    }
                });
        await().atMost(5, TimeUnit.SECONDS)
                .until(() -> count[0] == THREAD_COUNT);
        Assert.assertEquals(count[0], THREAD_COUNT);
    }
}
于 2019-03-28T19:15:55.000 に答える
13

Tudor の回答は問題ありませんが、静的でスケーラブルではありません。私のソリューションは動的でスケーラブルですが、実装の複雑さが増します。Lockこのクラスはインターフェイスを実装しているため、外の世界は を使用するのと同じようにこのクラスを使用できます。ファクトリ メソッドによって、パラメータ化されたロックのインスタンスを取得しますgetCanonicalParameterLock

package lock;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public final class ParameterLock implements Lock {

    /** Holds a WeakKeyLockPair for each parameter. The mapping may be deleted upon garbage collection
     * if the canonical key is not strongly referenced anymore (by the threads using the Lock). */
    private static final Map<Object, WeakKeyLockPair> locks = new WeakHashMap<>();

    private final Object key;
    private final Lock lock;

    private ParameterLock (Object key, Lock lock) {
        this.key = key;
        this.lock = lock;
    }

    private static final class WeakKeyLockPair {
        /** The weakly-referenced parameter. If it were strongly referenced, the entries of
         * the lock Map would never be garbage collected, causing a memory leak. */
        private final Reference<Object> param;
        /** The actual lock object on which threads will synchronize. */
        private final Lock lock;

        private WeakKeyLockPair (Object param, Lock lock) {
            this.param = new WeakReference<>(param);
            this.lock = lock;
        }
    }

    public static Lock getCanonicalParameterLock (Object param) {
        Object canonical = null;
        Lock lock = null;

        synchronized (locks) {
            WeakKeyLockPair pair = locks.get(param);            
            if (pair != null) {                
                canonical = pair.param.get(); // could return null!
            }
            if (canonical == null) { // no such entry or the reference was cleared in the meantime                
                canonical = param; // the first thread (the current thread) delivers the new canonical key
                pair = new WeakKeyLockPair(canonical, new ReentrantLock());
                locks.put(canonical, pair);
            }
        }

        // the canonical key is strongly referenced now...
        lock = locks.get(canonical).lock; // ...so this is guaranteed not to return null
        // ... but the key must be kept strongly referenced after this method returns,
        // so wrap it in the Lock implementation, which a thread of course needs
        // to be able to synchronize. This enforces a thread to have a strong reference
        // to the key, while it isn't aware of it (as this method declares to return a 
        // Lock rather than a ParameterLock).
        return new ParameterLock(canonical, lock);               
    }

    @Override
    public void lock() {
        lock.lock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        lock.lockInterruptibly();
    }

    @Override
    public boolean tryLock() {
        return lock.tryLock();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return lock.tryLock(time, unit);
    }

    @Override
    public void unlock() {
        lock.unlock();
    }

    @Override
    public Condition newCondition() {
        return lock.newCondition();
    }
}

もちろん、特定のパラメーターの正規キーが必要です。そうしないと、スレッドが別のロックを使用するため、スレッドが同期されません。正規化は、Tudor のソリューションにおける文字列の内部化に相当します。それ自体はスレッドセーフString.intern()ですが、私の「標準プール」はスレッドセーフではないため、WeakHashMap で追加の同期が必要です。

このソリューションは、あらゆるタイプのオブジェクトに対して機能します。ただし、カスタム クラスで正しく実装するようにしてくださいequalshashCodeそうしないと、複数のスレッドが異なる Lock オブジェクトを使用して同期する可能性があるため、スレッド化の問題が発生する可能性があります。

WeakHashMap の選択は、それがもたらすメモリ管理の容易さによって説明されます。特定のロックを使用しているスレッドがもうないことを他にどのように知ることができますか? そして、これがわかった場合、どうすればそのエントリをマップから安全に削除できるでしょうか? ロックを使用したい到着スレッドとマップからロックを削除するアクションとの間に競合状態があるため、削除時に同期する必要があります。これらはすべて弱参照を使用することで解決されるため、VM が作業を行い、実装が大幅に簡素化されます。WeakReference の API を調べると、弱参照に依存することがスレッドセーフであることがわかります。

次に、このテスト プログラムを調べます (一部のフィールドはプライベートに表示されるため、ParameterLock クラス内から実行する必要があります)。

public static void main(String[] args) {
    Runnable run1 = new Runnable() {

        @Override
        public void run() {
            sync(new Integer(5));
            System.gc();
        }
    };
    Runnable run2 = new Runnable() {

        @Override
        public void run() {
            sync(new Integer(5));
            System.gc();
        }
    };
    Thread t1 = new Thread(run1);
    Thread t2 = new Thread(run2);

    t1.start();
    t2.start();

    try {
        t1.join();
        t2.join();
        while (locks.size() != 0) {
            System.gc();
            System.out.println(locks);
        }
        System.out.println("FINISHED!");
    } catch (InterruptedException ex) {
        // those threads won't be interrupted
    }
}

private static void sync (Object param) {
    Lock lock = ParameterLock.getCanonicalParameterLock(param);
    lock.lock();
    try {
        System.out.println("Thread="+Thread.currentThread().getName()+", lock=" + ((ParameterLock) lock).lock);
        // do some work while having the lock
    } finally {
        lock.unlock();
    }        
}

両方のスレッドが同じロック オブジェクトを使用しているため、それらが同期されている可能性が非常に高いです。出力例:

Thread=Thread-0, lock=java.util.concurrent.locks.ReentrantLock@8965fb[Locked by thread Thread-0]
Thread=Thread-1, lock=java.util.concurrent.locks.ReentrantLock@8965fb[Locked by thread Thread-1]
FINISHED!

ただし、2 つのスレッドが実行時にオーバーラップしない可能性があるため、同じロックを使用する必要はありません。適切な場所にブレークポイントを設定し、必要に応じて最初または 2 番目のスレッドを強制的に停止することにより、デバッグ モードでこの動作を簡単に適用できます。また、メイン スレッドでガベージ コレクションが実行された後、WeakHashMap がクリアされることにも気付くでしょう。これはもちろん正しいことです。Thread.join()ガベージコレクタを呼び出す前に。これは実際、(Parameter)Lock への強い参照がワーカー スレッド内に存在できなくなることを意味するため、弱いハッシュマップから参照をクリアできます。別のスレッドが同じパラメータで同期する必要がある場合は、新しいロックが の同期部分に作成されgetCanonicalParameterLockます。

次に、同じ正準表現を持つ (= 等しい、a.equals(b)つまり ) ペアでテストを繰り返し、それでも機能することを確認します。

sync("a");
sync(new String("a"))

sync(new Boolean(true));
sync(new Boolean(true));

基本的に、このクラスは次の機能を提供します。

  • パラメータ化された同期
  • カプセル化されたメモリ管理
  • あらゆるタイプのオブジェクトを操作する能力 (適切に実装されequalsているという条件の下で)hashCode
  • Lock インターフェイスを実装します

この Lock 実装は、ArrayList を 1000 回反復する 10 スレッドで同時に変更することによってテストされています。2 つの項目を追加し、完全なリストを反復して最後に見つかったリスト エントリを削除します。反復ごとにロックが要求されるため、合計で 10*1000 のロックが要求されます。ConcurrentModificationException はスローされず、すべてのワーカー スレッドが終了した後、項目の合計数は 10*1000 でした。単一の変更ごとに、 を呼び出すことによってロックが要求されたParameterLock.getCanonicalParameterLock(new String("a"))ため、正規化の正確性をテストするために新しいパラメーター オブジェクトが使用されます。

パラメータに文字列リテラルとプリミティブ型を使用しないでください。文字列リテラルは自動的にインターンされるため、常に強力な参照を持ちます。そのため、最初のスレッドがそのパラメーターの文字列リテラルと共に到着した場合、ロック プールはエントリから解放されず、メモリ リークが発生します。同じことがオートボクシング プリミティブにも当てはまります。たとえば、Integer には、オートボクシングのプロセス中に既存の Integer オブジェクトを再利用するキャッシュ メカニズムがあり、強力な参照も存在します。ただし、これに対処する場合、これは別の話です。

于 2015-02-05T15:29:48.917 に答える
1

このフレームワークをチェックしてください。このようなものを探しているようです。

public class WeatherServiceProxy {
...
private final KeyLockManager lockManager = KeyLockManagers.newManager();

public void updateWeatherData(String cityName, Date samplingTime, float temperature) {
        lockManager.executeLocked(cityName, new LockCallback() {
                public void doInLock() {
                        delegate.updateWeatherData(cityName, samplingTime, temperature);
                }
        });
}

https://code.google.com/p/jkeylockmanager/

于 2014-07-16T12:39:26.097 に答える