当从多个线程以相反顺序执行equals()时,Java的同步集合出现问题

当从多个线程以相反顺序执行equals()时,Java的同步集合出现问题,java,collections,deadlock,Java,Collections,Deadlock,示例场景: 创建两个同步集(s1和s2) 将它们传递给两个线程(T1和T2) 启动线程 T1的run(): 而(永远) s1.等于(s2) T2的run() 而(永远) s2.等于(s1) 会发生什么? -SynchronizedSet的equals获得自身的锁 它计算传入的参数的长度以及它包含的内容,以确定其是否相等[注意:这是基于我分析的日志的猜测] 如果传入的参数也是SynchronizedSet,那么对size()和containAll()的调用也意味着必须获取该参数的锁 在上例中

示例场景

  • 创建两个同步集(s1和s2)
  • 将它们传递给两个线程(T1和T2)
  • 启动线程
T1的run(): 而(永远) s1.等于(s2)

T2的run() 而(永远) s2.等于(s1)

会发生什么? -SynchronizedSet的equals获得自身的锁

  • 它计算传入的参数的长度以及它包含的内容,以确定其是否相等[注意:这是基于我分析的日志的猜测]

  • 如果传入的参数也是SynchronizedSet,那么对size()和containAll()的调用也意味着必须获取该参数的锁

  • 在上例中,T1和T2的锁获取顺序如下:

    T1:s1->s2 T2:s2->s1

Ofc,这会导致僵局

此问题并非仅限于同步集合。即使使用哈希表或向量,也可能发生这种情况


我认为这是一个JavaAPI限制(设计)。如何克服这个问题?如何确保我的应用程序中不会发生这种情况?在不陷入这种情况的情况下,我是否应该遵循一些设计原则?

您可以在每个线程中以相同的顺序锁定两个集合:

            synchronized(s1) {
                synchronized(s2) {
                    s1.equals(s2);
                }
            }


我可能建议使用synchronized(){]块

大概是这样的:

while(forever){
    synchronized(s1){
        s1.equals(s2);
    }
}

我认为这是一个JavaAPI限制(设计)

我相信你错了。我曾经使用过的每个PL级锁定方案的一个基本要求是线程必须以相同的顺序锁定资源,否则会出现死锁。这同样适用于数据库

事实上,我认为你能避免这种情况的唯一方法是:

  • 要求应用程序获取单个原子操作中所需的所有锁,或
  • 使用单个全局锁执行所有锁定
这两种方法都是不切实际和不可扩展的

如何克服这个问题


编写应用程序代码,使所有线程以相同的顺序获得锁。@Maurice和@Nettogrof的答案给出了如何实现这一点的示例,但如果需要担心大量集合,则可能会更困难。

Stephen C的方法很好。进一步信息:如果不知道集合的方向,可以使用无论何时比较两个集合,都会出现“全局”锁定:

 private static final Object lock = new Object(); // May be context-local.

 [...]

     synchronized (lock) {
         synchronized (s1) {
             synchronized (s2) {
                 return s1.equals(s2);
             }
          }
     }
如果这些集合可能会被争用,您可以在大多数情况下按标识哈希代码排序,并返回到全局锁:

    int h1 = System.identityHashCode(s1);
    int h2 = System.identityHashCode(s2);
    return
         h1<h2 ? lockFirstEquals(h1, h2) :
         h2<h1 ? lockFirstEquals(h2, h1) :
         globalLockEquals(h1, h2);

最好的解决方案可能是更改线程策略,这样您就不需要在此处进行任何锁定。

在执行
equals()之前,您可以按它们的顺序对集合进行排序
调用。这样,锁获取的顺序将始终保持不变。

@Tom:但请注意,如果不同时获取辅助锁,您仍然可能会遇到主锁的问题。例如,如果某个已经持有
s2
锁的线程执行第一个代码sni,则可能会出现死锁事实上,我认为你的第一个代码片段没有抓住我的重点。我想说的是,保证没有死锁的方法之一是对所有数据结构使用一个且只有一个锁。当然,这是不切实际的,因为(首先)它不可伸缩。如果一个线程已经持有锁并调用随机代码,它无论如何都会遇到麻烦。我想如果你使用
java.util.concurrent.locks
,你可以对它进行编码,但我会尝试编写更合理的代码。@Tom的解决方案暗示了你的解决方案的问题。有一个很小但有限的概率至少,这两个集合将具有相同的标识哈希代码值。由于您实际获得的哈希值集合有限,“生日悖论”,在一次运行中多次执行该操作并在多台机器上运行,因此它实际上会经常发生。尽管您可能没有注意到,但并非所有潜在的死锁事件都会死锁。
 private static final Object lock = new Object(); // May be context-local.

 [...]

     synchronized (lock) {
         synchronized (s1) {
             synchronized (s2) {
                 return s1.equals(s2);
             }
          }
     }
    int h1 = System.identityHashCode(s1);
    int h2 = System.identityHashCode(s2);
    return
         h1<h2 ? lockFirstEquals(h1, h2) :
         h2<h1 ? lockFirstEquals(h2, h1) :
         globalLockEquals(h1, h2);
public StringBuffer append(StringBuffer other) {
    if (other == null) {
        return append("null");
    }
    int thisHash  = System.identityHashCode(this);
    int otherHash = System.identityHashCode(other);
    if (thisHash < otherHash) {
        synchronized (this) {
            synchronized (other) {
                appendImpl(other);
            }
        }
    } else if (otherHash < thisHash) {
        synchronized (other) {
            synchronized (this) {
                appendImpl(other);
            }
        }
    } else {
        append(other.toString()); // Or append((Object)other);
    }
    return this;
}