【问题标题】:Is there a way to synchronize using two lock objects in Java?有没有办法在 Java 中使用两个锁对象进行同步?
【发布时间】:2011-12-20 14:54:20
【问题描述】:

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

例如如果我有 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,同时进行多个活动的可能性要小得多,所以我很少会过度锁定。 (但不能对相反的方向说同样的话。)

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

谢谢!

【问题讨论】:

    标签: java multithreading synchronized


    【解决方案1】:

    让每个DatabaseIdentifier 保留一组锁定到它拥有的ActivityIdentifiers 的锁

    所以你可以打电话

    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 上的全部操作要好(尤其是当它是一个长操作时),但仍然比您的理想情况更糟

    响应 cmets 关于 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);
    }
    

    【讨论】:

    • 这对我来说效果很好。 ActivityIdentifier 对象实际上是一组预定义的静态最终对象,而 DatabaseIdentifier 对象不是。所以每次我生成一个新的 DatabaseIdentifier 对象时,我都可以轻松设置所有活动锁。
    • 您的代码存在一个问题。 return locks.puIfAbsent(actID,new Object()); 如果 Thread 在 put 中成功,将返回 null。否则 +1
    • @JohnVint 我用一些杂物修复了它;)
    • ActivityIdentifier 真的可以是一个枚举吗?如果是这样,您可以在DatabaseIdentifier 中使用(预填充的)EnumMap,它与地图一样快速和紧凑。
    • @TomAnderson 和只读访问不需要同步
    【解决方案2】:

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

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

    然后锁定该对象。享元工厂可以在 map 上获取读锁以查看密钥是否在其中,如果不存在则只做写锁。它应该减少并发因素。

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

    【讨论】:

    • 享元工厂不是必须准确解决促使 Jeffrey 提出这个问题的锁争用问题吗?这不就是把问题封装在一个抽象层吗?
    • @TomAnderson 他说“我可以创建一个hashmap...”,我只是说他应该这样做。我确实建议他对 hashmap 使用读锁,这样可以减少争用。
    【解决方案3】:

    我想不出一种方法来真正捕捉您锁定一对对象的想法。一些低级并发研究人员可能能够发明一个,但我怀疑我们是否有必要的原语在 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();
    }
    

    在 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。

    【讨论】:

    • 这看起来很像我想要完成的。我无法确定如何进行无锁锁定查找。无论如何,为 LockGuffins +1。
    • 例如,AWTInvocationLock。
    【解决方案4】:

    您的 hashmap 建议是我过去所做的。我要做的唯一更改是使用 ConcurrentHashMap,以最小化同步。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-04-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-27
      • 2023-04-09
      • 2018-09-14
      • 1970-01-01
      相关资源
      最近更新 更多