我想知道Java中是否有办法使用两个锁定对象进行同步。 我并不是说锁定 对象,我的意思是只锁定两个。
e.g。如果我有4个帖子:
在上面的场景中,线程A和线程D将共享一个锁,但是线程B和线程C将拥有自己的锁。即使它们与两个对象中的一个重叠,但只有在两个对象重叠时才会应用相同的锁。
所以我有一个由许多线程调用的方法,它将根据特定的数据库执行特定的活动类型。我有数据库和活动的标识符对象,我可以保证该操作是线程安全的,只要它与基于与另一个线程相同的数据库的活动不同。
我理想的代码看起来像:
public void doActivity(DatabaseIdentifier dbID, ActivityIdentifier actID) {
synchronized( dbID, actID ) { // <--- Not real Java
// Do an action that can be guaranteed thread-safe per unique
// combination of dbIT and actID, but needs to share a
// lock if they are both the same.
}
}
我可以创建一个由DatabaseIdentifier和ActivityIdentifier键入的锁对象的hashmap,但是当我需要以线程安全的方式创建/访问这些锁时,我将遇到同样的同步问题。
现在我只是在同步DatabaseIdentifier。对于一个DBIdentifier,同时进行多个活动的可能性要小得多,因此我很少会过度锁定。 (但不能说相反的方向相同。)
任何人都有一个很好的方法来处理这个问题,而不会强迫不必要的线程等待吗?
谢谢!
答案 0 :(得分:4)
让每个DatabaseIdentifier
保留一组锁定为其所拥有的ActivityIdentifier
的锁
所以你可以打电话给
public void doActivity(DatabaseIdentifier dbID, ActivityIdentifier actID) {
synchronized( dbID.getLock(actID) ) {
// Do an action that can be guaranteed thread-safe per unique
// combination of dbIT and actID, but needs to share a
// lock if they are both the same.
}
}
那么你只需要在dbID
换句话说
ConcurrentHashMap<ActivityIdentifier ,Object> locks = new...
public Object getLock(ActivityIdentifier actID){
Object res = locks.get(actID); //avoid unnecessary allocations of Object
if(res==null) {
Object newLock = new Object();
res = locks.puIfAbsent(actID,newLock );
return res!=null?res:newLock;
} else return res;
}
这比在dbID上锁定完整操作更好(特别是在长时间操作时),但仍比理想情况更差
更新以响应有关EnumMap的评论
private final EnumMap<ActivityIdentifier ,Object> locks;
/**
initializer ensuring all values are initialized
*/
{
EnumMap<ActivityIdentifier ,Object> tmp = new EnumMap<ActivityIdentifier ,Object>(ActivityIdentifier.class)
for(ActivityIdentifier e;ActivityIdentifier.values()){
tmp.put(e,new Object());
}
locks = Collections.unmodifiableMap(tmp);//read-only view ensures no modifications will happen after it is initialized making this thread-safe
}
public Object getLock(ActivityIdentifier actID){
return locks.get(actID);
}
答案 1 :(得分:2)
我认为你应该采用hashmap的方式,但将其封装在flyweight工厂中。即,你打电话:
FlyweightAllObjectsLock lockObj = FlyweightAllObjectsLock.newInstance(dbID, actID);
然后锁定该对象。 flyweight工厂可以在地图上获取读锁定,以查看该键是否在那里,如果不是,则只执行写锁定。它应该减少并发因素。
您可能还希望在该映射中使用弱引用,以避免将内存从垃圾回收中保留。
答案 2 :(得分:2)
我想不出一种能够真正捕捉到锁定一对物体的想法的方法。一些低级并发boffin可能能够发明一个,但我怀疑我们是否有必要的原语来用Java实现它。
我认为使用对作为键来识别锁定对象的想法是一个很好的想法。如果要避免锁定,请排列查找,使其不执行任何操作。
我会建议一个两级地图,模糊地说:
Map<DatabaseIdentifier, Map<ActivityIdentifier, Lock>> locks;
因此模糊地使用:
synchronized (locks.get(databaseIdentifier).get(activityIdentifier)) {
performSpecificActivityOnDatabase();
}
如果您知道所有数据库和活动是预先确定的,那么只需在应用程序启动时创建一个包含所有组合的完美法线贴图,并完全按照上述方式使用它。唯一的锁定在锁定对象上,并且没有争用。
如果您不知道数据库和活动是什么,或者预先创建完整地图的组合太多,则需要逐步创建地图。这就是Concurrency Fun Times的开始。
直截了当的解决方案是懒洋洋地创建内部地图和锁,并使用普通锁保护这些操作:
Map<ActivityIdentifier, Object> locksForDatabase;
synchronized (locks) {
locksForDatabase = locks.get(databaseIdentifier);
if (locksForDatabase == null) {
locksForDatabase = new HashMap<ActivityIdentifier, Object>();
locks.put(databaseIdentifier, locksForDatabase);
}
}
Object lock;
synchronized (locksForDatabase) {
lock = locksForDatabase.get(locksForDatabase);
if (lock == null) {
lock = new Object();
locksForDatabase.put(locksForDatabase, lock);
}
}
synchronized (lock) {
performSpecificActivityOnDatabase();
}
正如您明显意识到的那样,这将导致过多的争用。我只提到说教的完整性。
您可以通过使外部地图并发来改进它:
ConcurrentMap<DatabaseIdentifier, Map<ActivityIdentifier, Object>> locks;
和
Map<ActivityIdentifier, Object> newHashMap = new HashMap<ActivityIdentifier, Object>();
Map<ActivityIdentifier, Object> locksForDatabase = locks.putIfAbsent(databaseIdentifier, newHashMap);
if (locksForDatabase == null) locksForDatabase = newHashMap;
Object lock;
synchronized (locksForDatabase) {
lock = locksForDatabase.get(locksForDatabase);
if (lock == null) {
lock = new Object();
locksForDatabase.put(locksForDatabase, lock);
}
}
synchronized (lock) {
performSpecificActivityOnDatabase();
}
您的唯一锁争用将出现在每个数据库的映射中,在put和get的持续时间内,并且根据您的报告,将不会有太多这样的。您可以将内部地图转换为ConcurrentMap
以避免这种情况,但这听起来有点过分。
但是,会创建稳定的HashMap
个实例流,以便将其提供给putIfAbsent
然后被丢弃。你可以通过一种双重检查锁定的后现代原子混合来避免这种情况;用以下代码替换前三行:
Map<ActivityIdentifier, Object> locksForDatabase = locks.get(databaseIdentifier);
if (locksForDatabase == null) {
Map<ActivityIdentifier, Object> newHashMap = new HashMap<ActivityIdentifier, Object>();
locksForDatabase = locks.putIfAbsent(databaseIdentifier, newHashMap);
if (locksForDatabase == null) locksForDatabase = newHashMap;
}
在每个数据库映射已经存在的常见情况下,这将执行单个并发get
。在不常见的情况下,它会执行额外但必要的new HashMap()
和putIfAbsent
。在非常罕见的情况下,它没有,但另一个线程也发现,其中一个线程将执行冗余new HashMap()
和putIfAbsent
。这不应该是昂贵的。
实际上,我觉得这是一个很糟糕的想法,你应该将这两个标识符放在一起制作一个双倍大小的密钥,并使用它来在一个ConcurrentHashMap
中进行查找。可悲的是,我太懒了,也无法删除上面的内容。考虑这个建议是迄今为止阅读的特别奖。
PS看到一个Object的实例只是一个锁,它总是让我感到懊恼。我建议给他们打电话LockGuffins。
答案 3 :(得分:0)
您的hashmap建议是我过去所做的。我所做的唯一改变是使用ConcurrentHashMap来最小化同步。
另一个问题是如果可能的密钥将要改变,如何清理地图。