Tudor 的答案很好,但它是静态的,不可扩展。我的解决方案是动态的和可扩展的,但它的实现复杂性增加了。外界可以像使用 a 一样使用这个类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!
但是,有可能这两个线程在执行中不重叠,因此不需要它们使用相同的锁。通过在正确的位置设置断点,您可以轻松地在调试模式下强制执行此行为,强制第一个或第二个线程在必要时停止。您还会注意到,在主线程上的垃圾收集之后,WeakHashMap 将被清除,这当然是正确的,因为主线程通过调用等待两个工作线程完成它们的工作Thread.join()
在调用垃圾收集器之前。这确实意味着在工作线程中不再存在对 (Parameter)Lock 的强引用,因此可以从弱 hashmap 中清除该引用。如果另一个线程现在想要在同一个参数上同步,一个新的 Lock 将在同步部分中创建getCanonicalParameterLock
。
现在用任何具有相同规范表示的对重复测试(= 它们相等,所以a.equals(b)
),看看它仍然有效:
sync("a");
sync(new String("a"))
sync(new Boolean(true));
sync(new Boolean(true));
等等
基本上,此类为您提供以下功能:
- 参数化同步
- 封装内存管理
- 使用任何类型的对象的能力(在正确实施的条件下
equals
)hashCode
- 实现锁接口
此 Lock 实现已通过同时修改 ArrayList 进行测试,其中 10 个线程迭代 1000 次,执行以下操作:添加 2 个项目,然后通过迭代完整列表删除最后找到的列表条目。每次迭代都会请求一个锁,因此总共将请求 10*1000 个锁。没有抛出 ConcurrentModificationException,并且在所有工作线程完成后,项目总数为 10*1000。在每一次修改中,调用都会请求一个锁ParameterLock.getCanonicalParameterLock(new String("a"))
,因此使用一个新的参数对象来测试规范化的正确性。
请注意,您不应该对参数使用字符串文字和原始类型。由于 String 字面量是自动实习的,它们总是有一个强引用,因此如果第一个线程到达时带有一个 String 字面量作为其参数,那么锁池将永远不会从条目中释放出来,这就是内存泄漏。自动装箱原语也是如此:例如,Integer 有一个缓存机制,该机制将在自动装箱过程中重用现有的 Integer 对象,这也会导致强引用的存在。然而,解决这个问题,这是一个不同的故事。