繁体   English   中英

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

[英]Is there a way to synchronize using two lock objects in Java?

我想知道Java中是否有一种方法可以使用两个锁定对象进行同步。 我的意思不是锁定任何物体上时,我的意思是只有两个锁定。

例如,如果我有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键入的锁对象的哈希映射,但是当我需要以线程安全的方式创建/访问这些锁时,我将遇到同样的同步问题。

现在我只是在DatabaseIdentifier上进行同步。 对于一个DBIdentifier,同时进行多个活动的可能性要小得多,因此我很少会过度锁定。 (但不能说相反的方向相同。)

任何人都有一个很好的方法来处理这个不涉及强迫不必要的线程等待?

谢谢!

让每个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);
}

我认为你应该采用hashmap的方式,但将其封装在flyweight工厂中。 即,你打电话:

FlyweightAllObjectsLock lockObj = FlyweightAllObjectsLock.newInstance(dbID, actID);

然后锁定该对象。 flyweight工厂可以在地图上获取读锁定,以查看该键是否在那里,如果不是,则只执行写锁定。 它应该减少并发因素。

您可能还希望在该映射上使用弱引用,以避免将内存保留在垃圾回收中。

我想不出这样做的方法真正抓住你锁定一对物体的想法。 一些低级并发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

你的hashmap建议就是我过去所做的。 我所做的唯一改变是使用ConcurrentHashMap来最小化同步。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM