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

发布于 2024-12-13 06:26:11 字数 1060 浏览 1 评论 0原文

我想知道Java中是否有一种方法可以使用两个锁对象进行同步。 我的意思不是锁定任一对象,而是仅锁定两者

例如,如果我有 4 个线程:

  • 线程 A 使用对象 1 和对象 2 请求锁
  • 线程 B 使用对象 1 和对象 3 请求锁
  • 线程 C 使用对象 4 和对象 2 请求锁
  • 线程 D 使用对象 1 和对象 2 请求锁

在上述场景中,线程 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键控的锁对象的散列图,但是当我需要在a中创建/访问这些锁时,我将遇到相同的同步问题。线程安全的方式。

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

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

谢谢!

I'm wondering if there's a way in Java to synchronize using two lock objects.
I don't mean locking on either object, I mean locking only on both.

e.g. if I have 4 threads:

  • Thread A requests a lock using Object1 and Object2
  • Thread B requests a lock using Object1 and Object3
  • Thread C requests a lock using Object4 and Object2
  • Thread D requests a lock using Object1 and Object2

In the above scenario, Thread A and Thread D would share a lock, but Thread B and Thread C would have their own locks. Even though they overlap with one of the two objects, the same lock only applies if it overlaps on both.

So I have a method called by many threads which is going to perform a specific activity type based on a specific database. I have identifier objects for both the database and the activity, and I can guarantee that the action will be thread safe as long as it is not the same activity based on the same database as another thread.

My ideal code would look something like:

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.
    }
}

I could create a hashmap of lock objects that are keyed by both the DatabaseIdentifier and the ActivityIdentifier, but I'm going to run into the same synchronization issue when I need to create/access those locks in a thread-safe way.

For now I'm just synchronizing on the DatabaseIdentifier. It's much less likely that there will be multiple activities going on at the same time for one DBIdentifier, so I will only rarely be over-locking. (Can't say the same for the opposite direction though.)

Anyone have a good way to handle this that doesn't involve forcing unnecessary threads to wait?

Thanks!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

善良天后 2024-12-20 06:26:11

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

,然后您只需要在底层集合上有一个(短)锁(使用ConcurrentHashMap )在 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);
}

have each DatabaseIdentifier keep a set of locks keyed to ActivityIdentifiers that it owns

so you can call

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.
    }
}

then you only need a (short) lock on the underlying collection (use a ConcurrentHashMap) in dbID

in other words

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;
}

this is better than locking the full action on dbID (especially when its a long action) but still worse than your ideal scenario

update in responce to comments about 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);
}
合久必婚 2024-12-20 06:26:11

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

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

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

您可能还想研究在该映射上使用弱引用,以避免内存被垃圾回收。

I think you should go the way of the hashmap, but encapsulate that in a flyweight factory. Ie, you call:

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

Then lock on that object. The flyweight factory can get a read lock on the map to see if the key is in there, and only do a write lock if it is not. It should reduce the concurrency factor.

You might also want to look into using weak references on that map as well, to avoid keeping memory from garbage collection.

じее 2024-12-20 06:26:11

我想不出一种方法可以真正体现您锁定一对对象的想法。一些低级并发研究人员可能能够发明一个,但我怀疑我们是否有必要的原语在 Java 中实现它。

我认为使用这些对作为密钥来识别锁对象的想法是一个很好的想法。如果您想避免锁定,请安排查找以使其不执行任何操作。

我建议使用一个两级映射,模糊地类似于:

Map<DatabaseIdentifier, Map<ActivityIdentifier, Lock>> locks;

模糊地使用:

synchronized (locks.get(databaseIdentifier).get(activityIdentifier)) {
    performSpecificActivityOnDatabase();
}

如果您预先知道所有数据库和活动是什么,那么只需在应用程序启动时创建一个包含所有组合的完全正常的映射,然后将其完全用作多于。唯一的锁定是在锁定对象上,并且不存在争用。

如果您不知道数据库和活动是什么,或者有太多组合无法预先创建完整的地图,那么您将需要增量创建地图。这就是并发欢乐时光的开始。

最简单的解决方案是延迟创建内部映射和锁,并使用普通锁保护这些操作:

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();
}

在放置和获取期间,每个数据库映射上将存在唯一的锁争用,并且根据您的报告,不会有太多那。您可以将内部映射转换为 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

I can't think of a way to do this that really captures your idea of locking a pair of objects. Some low-level concurrency boffin might be able to invent one, but i have my doubts about whether we would have the necessary primitives to implement it in Java.

I think the idea of using the pairs as keys to identify lock objects is a good one. If you want to avoid locking, then arrange the lookup so that it doesn't do any.

I would suggest a two-level map, vaguely like:

Map<DatabaseIdentifier, Map<ActivityIdentifier, Lock>> locks;

Used vaguely thus:

synchronized (locks.get(databaseIdentifier).get(activityIdentifier)) {
    performSpecificActivityOnDatabase();
}

If you know what all the databases and activities are upfront, then just create a perfectly normal map containing all the combinations when your application starts up, and use it exactly as above. The only locking is on the lock objects, and there is no contention.

If you don't know what the databases and activities will be, or there are too many combinations to create a complete map upfront, then you will need to create the map incrementally. This is where Concurrency Fun Times begin.

The straightforward solution is to lazily create the inner maps and the locks, and to protect these actions with normal locks:

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();
}

As you are evidently aware, this will lead to too much contention. I mention it only for didactic completeness.

You can improve it by making the outer map concurrent:

ConcurrentMap<DatabaseIdentifier, Map<ActivityIdentifier, Object>> locks;

And:

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();
}

Your only lock contention there will be on the per-database maps, for the duration of a put and a get, and according to your report, there won't be much of that. You could convert the inner map to a ConcurrentMap to avoid that, but that sounds like overkill.

There will, however, be a steady stream of HashMap instances being created to be fed to putIfAbsent and then being thrown away. You can avoid that with a sort of postmodern atomic remix of double-checked locking; replace the first three lines with:

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;
}

In the common case that the per-database map already exists, this will do a single concurrent get. In the uncommon case that it does not, it will do an additional but necessary new HashMap() and putIfAbsent. In the very rare case that it does not, but another thread has also discovered that, one of the threads will be doing a redundant new HashMap() and putIfAbsent. That should not be expensive.

Actually, it occurs to me that this is all a terrible idea, and that you should just stick the two identifiers together to make one double-size key, and use that to make lookups in a single ConcurrentHashMap. Sadly, i am too lazy and vain to delete the above. Consider this advice a special prize for reading this far.

PS It always mildly annoys me to see an instance of Object used as nothing but a lock. I propose calling them LockGuffins.

过期以后 2024-12-20 06:26:11

你的哈希图建议是我过去所做的。我要做的唯一改变是使用 ConcurrentHashMap,以最大限度地减少同步。

另一个问题是,如果可能的键发生变化,如何清理地图。

Your hashmap suggestion is what I've done in the past. The only change I'd make is using a ConcurrentHashMap, to minimize the synchronization.

The other issue is how to cleanup the map if the possible keys are going to change.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文