有没有办法在Java中使用两个锁定对象进行同步?

时间:2011-11-02 19:06:57

标签: java multithreading synchronized

我想知道Java中是否有办法使用两个锁定对象进行同步。 我并不是说锁定 对象,我的意思是只锁定两个

e.g。如果我有4个帖子:

  • 线程A使用Object1和Object2
  • 请求锁定
  • 线程B使用Object1和Object3
  • 请求锁定
  • 线程C使用Object4和Object2
  • 请求锁定
  • 线程D使用Object1和Object2
  • 请求锁定

在上面的场景中,线程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,同时进行多个活动的可能性要小得多,因此我很少会过度锁定。 (但不能说相反的方向相同。)

任何人都有一个很好的方法来处理这个问题,而不会强迫不必要的线程等待吗?

谢谢!

4 个答案:

答案 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)进行(短)锁定

换句话说

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来最小化同步。

另一个问题是如果可能的密钥将要改变,如何清理地图。