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 で追加の同期が必要です。
このソリューションは、あらゆるタイプのオブジェクトに対して機能します。ただし、カスタム クラスで正しく実装するようにしてください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()
ガベージコレクタを呼び出す前に。これは実際、(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 オブジェクトを再利用するキャッシュ メカニズムがあり、強力な参照も存在します。ただし、これに対処する場合、これは別の話です。