Javaパラメータに基づいて同期する(ミューテックス/ロックという名前)
受け取ったパラメーターに基づいてメソッドを同期する方法を探しています。次のようなものです。
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);
}
ロックマップにデータを入力しない理由は、名前には任意の値を設定できるためです。
上記のサンプルに基づくと、HashMapはスレッドセーフではないため、複数のスレッドによって同時にハッシュマップから値を追加/削除すると、問題が発生する可能性があります。
だから私の質問は、スレッドセーフなHashMap
をConcurrentHashMap
にすると、同期されたブロックは他のスレッドがlocks.get(name)にアクセスするのを止めますか?
マップを使用して、文字列をロックオブジェクトに関連付けます。
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)) {
// ...
}
}
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();
}
}
_
もちろん、特定のパラメーターには正規キーが必要です。そうでない場合、スレッドは別のロックを使用するため、同期されません。正規化は、チューダーのソリューションにおける文字列の内部化と同等です。 String.intern()
自体がスレッドセーフであるのに対し、私の「正規プール」はそうではないため、WeakHashMapで追加の同期が必要です。
このソリューションは、あらゆるタイプのオブジェクトに対して機能します。ただし、カスタムクラスにequals
とhashCode
を正しく実装するようにしてください。そうしないと、複数のスレッドが異なる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()
を呼び出してジョブを終了するのを待ってから呼び出すため、もちろん正しいです。ガベージコレクター。これは確かに、(パラメーター)ロックへの強い参照がワーカースレッド内に存在できなくなったことを意味するため、弱いハッシュマップから参照をクリアできます。別のスレッドが同じパラメーターで同期したい場合は、getCanonicalParameterLock
の同期された部分に新しいロックが作成されます。
ここで、同じ正規表現を持つ任意のペアでテストを繰り返し(=それらは等しいので、a.equals(b)
)、それがまだ機能することを確認します。
_sync("a");
sync(new String("a"))
sync(new Boolean(true));
sync(new Boolean(true));
_
等.
基本的に、このクラスは次の機能を提供します。
- パラメータ化された同期
- カプセル化されたメモリ管理
- 任意のタイプのオブジェクトを操作する機能(
equals
およびhashCode
が適切に実装されている場合) - Lockインターフェースを実装します
このロックの実装は、1000回繰り返される10個のスレッドと同時にArrayListを変更することによってテストされています。これは、2つの項目を追加し、最後に見つかったリストエントリを完全なリストを繰り返すことによって削除します。反復ごとにロックが要求されるため、合計で10 * 1000のロックが要求されます。 ConcurrentModificationExceptionはスローされず、すべてのワーカースレッドが終了した後、アイテムの合計量は10 * 1000でした。すべての変更で、ParameterLock.getCanonicalParameterLock(new String("a"))
を呼び出すことによってロックが要求されたため、新しいパラメーターオブジェクトを使用して、正規化の正確さをテストします。
パラメータに文字列リテラルとプリミティブ型を使用しないように注意してください。文字列リテラルは自動的にインターンされるため、常に強力な参照があります。したがって、最初のスレッドがそのパラメータに文字列リテラルを使用して到着した場合、ロックプールがエントリから解放されることはありません。これはメモリリークです。同じ話がオートボクシングプリミティブにも当てはまります。 Integerには、自動ボクシングのプロセス中に既存のIntegerオブジェクトを再利用するキャッシュメカニズムがあり、強力な参照も存在します。しかし、これに対処することは別の話です。
TL; DR:
Spring Frameworkの ConcurrentReferenceHashMap を使用します。以下のコードを確認してください。
このスレッドは古いですが、それでも面白いです。したがって、私のアプローチをSpringFrameworkと共有したいと思います。
私たちが実装しようとしているのは、という名前のミューテックス/ロックです。 チューダーの答え で示唆されているように、アイデアは、ロック名とロックオブジェクトを格納するためのMap
を持つことです。コードは次のようになります(私は彼の答えからコピーします):
_Map<String, Object> locks = new HashMap<String, Object>();
locks.put("a", new Object());
locks.put("b", new Object());
_
ただし、このアプローチには2つの欠点があります。
- OPはすでに最初のものを指摘しています:
locks
ハッシュマップへのアクセスを同期する方法は? - 不要になったロックを削除するにはどうすればよいですか?それ以外の場合、
locks
ハッシュマップは拡大し続けます。
最初の問題は、 ConcurrentHashMap を使用して解決できます。 2番目の問題については、2つのオプションがあります。手動でロックを確認してマップから削除するか、ガベージコレクターに使用されなくなったロックを通知してGCがそれらを削除します。私は2番目の方法で行きます。
HashMap
またはConcurrentHashMap
を使用すると、強力な参照が作成されます。上記のソリューションを実装するには、代わりに弱参照を使用する必要があります(強/弱参照とは何かを理解するには、 この記事 または この投稿 を参照してください)。
したがって、Spring Frameworkの ConcurrentReferenceHashMap を使用します。ドキュメントに記載されているように:
キーと値の両方にソフト参照または弱参照を使用する
ConcurrentHashMap
。このクラスは、同時にアクセスした場合のパフォーマンスを向上させるために、
Collections.synchronizedMap(new WeakHashMap<K, Reference<V>>())
の代わりに使用できます。この実装は、null値とnullキーがサポートされていることを除いて、ConcurrentHashMap
と同じ設計制約に従います。
これが私のコードです。 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);
}
}
_
私は別のstackoverflowの質問を通して適切な答えを見つけました: キーによってロックを取得する方法
私はここに答えをコピーしました:
Guavaには、このようなものが13.0でリリースされています。必要に応じて、HEADから取得できます。
Stripedは多かれ少なかれ特定の数のロックを割り当て、次にハッシュコードに基づいて文字列をロックに割り当てます。 APIは多かれ少なかれ似ています
Striped<Lock> locks = Striped.lock(stripes);
Lock l = locks.get(string);
l.lock();
try {
// do stuff
} finally {
l.unlock();
}
多かれ少なかれ、ストライプの数を制御できるため、同時実行性とメモリ使用量を交換できます。これは、各文字列キーに完全なロックを割り当てるとコストがかかる可能性があるためです。基本的に、ハッシュの衝突が発生した場合にのみロックの競合が発生しますが、これは(予想どおり)まれです。
このフレームワークを確認してください。あなたはこのようなものを探しているようです。
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);
}
});
}
McDowellの IdMutexProvider に基づいてtokenProviderを作成しました。マネージャーは、未使用のロックのクリーンアップを処理するWeakHashMap
を使用します。
あなたは私の実装を見つけることができます ここ 。
ロックオブジェクトを格納するためにキャッシュを使用しました。マイキャッシュは一定期間後にオブジェクトを期限切れにします。これは、同期されたプロセスの実行にかかる時間よりも長くする必要があるだけです。
`
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
...
private final Cache<String, Object> mediapackageLockCache = CacheBuilder.newBuilder().expireAfterWrite(DEFAULT_CACHE_EXPIRE, TimeUnit.SECONDS).build();
...
public void doSomething(foo) {
Object lock = mediapackageLockCache.getIfPresent(foo.toSting());
if (lock == null) {
lock = new Object();
mediapackageLockCache.put(foo.toString(), lock);
}
synchronized(lock) {
// execute code on foo
...
}
}
`
Guavas LoadingCache
とweakValues
を利用した、@ timmonspostに似たはるかに単純でスケーラブルな実装があります。 「平等」に関するヘルプファイルを読んで、私が行った提案を理解することをお勧めします。
次のweakValuedキャッシュを定義します。
private final LoadingCache<String,String> syncStrings = CacheBuilder.newBuilder().weakValues().build(new CacheLoader<String, String>() {
public String load(String x) throws ExecutionException {
return new String(x);
}
});
public void doSomething(String x) {
x = syncStrings.get(x);
synchronized(x) {
..... // whatever it is you want to do
}
}
今! JVMの結果として、キャッシュが大きくなりすぎることを心配する必要はありません。必要な期間だけキャッシュされた文字列を保持し、ガベージマネージャー/グアバが手間のかかる作業を行います。