12

我有以下仅包含一个字段的类i。对该字段的访问由对象的锁(“this”)保护。在实现 equals() 时,我需要锁定这个实例 (a) 和另一个 (b)。如果线程 1 调用 a.equals(b),同时线程 2 调用 b.equals(a),则两种实现中的锁定顺序是相反的,可能会导致死锁。

我应该如何为具有同步字段的类实现 equals()?

public class Sync {
    // @GuardedBy("this")
    private int i = 0;
    public synchronized int getI() {return i;}
    public synchronized void setI(int i) {this.i = i;}

    public int hashCode() {
        final int prime = 31;
        int result = 1;
        synchronized (this) {
            result = prime * result + i;
        }
        return result;
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Sync other = (Sync) obj;
        synchronized (this) {
            synchronized (other) {
                // May deadlock if "other" calls 
                // equals() on "this" at the same 
                // time 
                if (i != other.i)
                    return false;
            }
        }
        return true;
    }
}
4

14 回答 14

10

尝试在对象内部进行同步equalshashCode无法正常工作。考虑一个HashMap用于hashCode发现对象将在哪个“桶”中的情况,然后用于equals顺序搜索桶中的所有对象。

如果允许对象以改变结果的方式发生变异,hashCode或者equals您最终可能会遇到HashMap调用hashCode. 它获取锁,获取哈希并再次释放锁。 HashMap然后继续计算要使用的“桶”。但是之前HashMap可以获取锁等于其他人抢了锁并改变了对象,使得equals与之前的值不一致hashCode。这将导致灾难性的结果。

hashCodeequals 方法用在很多地方,是 Java 集合 API 的核心。重新考虑不需要同步访问这些方法的应用程序结构可能很有价值。或者至少不在对象本身上同步。

于 2009-10-28T10:41:13.753 回答
7

为什么要同步?如果它们在比较过程中发生变化,那么重要的用例是什么,如果在代码运行之前根据相等性立即发生变化,则无关紧要。(即,如果您的代码取决于平等,如果在此代码之前或期间值变得不相等,会发生什么情况)

我认为您必须查看更大的过程才能看到需要锁定的位置。

于 2009-10-28T10:35:23.620 回答
6

如果在离开同步后不能保证结果为真,那么同步 equals() 的重点在哪里:

if (o1.equals(o2)) {
  // is o1 still equal to o2?
}

因此,您可以简单地在不更改输出的情况下一个接一个地同步对 getI() 内部的调用 - 这很简单,不再有效。

您将始终必须同步整个块:

synchronized(o1) {
  synchronized(o2) {
    if (o1.equals(o2)) {
      // is o1 still equal to o2?
    }
  }
}

诚然,你仍然会面临同样的问题,但至少你在正确的点同步;)

于 2009-10-28T10:48:05.757 回答
3

如果已经说得够多了,您用于 hashCode()、equals() 或 compareTo() 的字段应该是不可变的,最好是 final。在这种情况下,您不需要同步它们。

实现 hashCode() 的唯一原因是可以将对象添加到哈希集合中,并且您不能有效地更改已添加到此类集合中的对象的 hashCode()。

于 2009-10-28T20:18:16.843 回答
2

您正在尝试在可变对象上定义基于内容的“equals”和“hashCode”。这不仅是不可能的:它没有意义。根据

http://java.sun.com/javase/6/docs/api/java/lang/Object.html

“equals”和“hashCode”都需要保持一致:对同一对象的连续调用返回相同的值。根据定义,可变性可以防止这种情况发生。这不仅仅是理论:许多其他类(例如集合)依赖于实现equals/hashCode的正确语义的对象。

同步问题在这里是一个红鲱鱼。当您解决潜在问题(可变性)时,您将不需要同步。如果您不解决可变性问题,那么再多的同步也无济于事。

于 2009-11-08T14:00:54.800 回答
1

始终以相同的顺序锁定它们,您可以根据以下结果确定顺序的一种方法System.identityHashCode(Object)

编辑以包含评论:

处理 identityHashCodes 相等的罕见情况的最佳解决方案需要更多关于这些对象的其他锁定的详细信息。

所有多对象锁定要求都应使用相同的解决过程。

您可以创建一个共享实用程序来在锁定要求的短时间内跟踪具有相同 identityHashCode 的对象,并在它们被跟踪的期间为它们提供可重复的排序。

于 2009-10-28T10:40:23.270 回答
1

确定同步是否绝对必要的唯一方法是分析整个程序的情况。您需要寻找两件事;一个线程正在更改对象而另一个线程正在调用 equals 的情况,以及线程调用equals可能会看到i.

如果同时锁定对象thisother对象,则确实存在死锁的风险。但我会质疑你需要这样做。相反,我认为你应该equals(Object)这样实现:

public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Sync other = (Sync) obj;
    return this.getI() == other.getI();
}

这并不能保证两个对象同时具有相同的值i,但这不太可能产生任何实际差异。equals毕竟,即使您确实有这样的保证,您仍然必须处理两个对象在调用返回时可能不再相等的问题。(这是@s的重点!)

此外,这并不能完全消除死锁的风险。考虑一个线程可能equals在持有两个对象之一的锁时调用的情况;例如

// In same class as above ...
public synchronized void frobbitt(Object other) {
    if (this.equals(other)) {
        ...
    }
}

现在如果两个线程分别调用a.frobbitt(b)and b.frobbitt(a),就有死锁的风险。

(但是,您确实需要调用getI()或声明i为 be volatile,否则如果它最近被不同的线程更新,则equals()可能会看到陈旧的值。)i

equals话虽如此,对于一个对象的基于值的方法,其组件值可能会发生变化,这是相当令人担忧的。例如,这将破坏许多集合类型。将此与多线程结合起来,您将很难确定您的代码是否真的正确。我不禁想到,最好更改equalsandhashcode方法,以便它们不依赖于在第一次调用方法后可能发生变化的状态。

于 2009-10-28T11:42:19.040 回答
1

(我假设您对这里的一般情况感兴趣,而不仅仅是包装整数。)

您不能阻止两个线程set以任意顺序调用 ... 方法。因此,即使一个线程true通过调用.equals(...获得(有效) ),该结果也可能被另一个set在其中一个对象上调用 ... 的线程立即无效。IOW 结果仅意味着在比较时值相等。

因此,同步将防止在您尝试进行比较时包装值处于不一致状态的情况(例如,连续更新int包装的两个大小的一半long)。您可以通过复制每个值(即独立同步,没有重叠)然后比较副本来避免竞争条件。

于 2009-10-28T11:56:35.383 回答
0

正如其他人所提到的,如果在 equals 检查期间事情发生了变化,那么已经有可能出现疯狂的行为(即使同步正确)。因此,您真正需要担心的是可见性(您要确保“发生在”您的 equals 调用之前的更改是可见的)。因此,您可以只做“快照”等于,这在“之前发生”关系方面是正确的,并且不会受到锁定排序问题的影响:

public boolean equals(Object o) {
  // ... standard boilerplate here ...

  // take a "snapshot" (acquire and release each lock in turn)
  int myI = getI();
  int otherI = ((Sync)o).getI();

  // and compare (no locks held at this point)
  return myI == otherI;
}
于 2011-09-12T00:54:13.487 回答
0

您需要确保对象在对 hashCode() 和 equals() 的调用(如果调用)之间不会发生变化。然后,您必须确保对象位于哈希映射中时对象不会更改(扩展至 hashCode 和 equals 所涉及的范围)。要更改对象,您必须先将其移除,然后将其更改并放回原处。

于 2011-09-12T00:26:19.730 回答
0

不要使用同步。想想不可修改的豆子。

于 2009-10-28T23:58:30.220 回答
0

哈希数据结构等各种事物都需要正确实现equals()and hashCode(),因此您在那里没有真正的选择。从另一个角度来看,equals()andhashCode()只是方法,对同步的要求与其他方法相同。你仍然有死锁问题,但它并不特定于它equals()导致它的事实。

于 2009-10-28T10:35:08.957 回答
0

正如 Jason Day 所指出的,整数比较已经是原子的,所以这里的同步是多余的。但是,如果您只是构建一个简化的示例并且在现实生活中您正在考虑一个更复杂的对象:

您的问题的直接答案是,确保您始终以一致的顺序比较项目。这个顺序是什么并不重要,只要它是一致的。在这种情况下,System.identifyHashCode 将提供一个排序,例如:

public boolean equals(Object o)
{
  if (this==o)
    return true;
  if (o==null || !o instanceof Sync)
    return false;
  Sync so=(Sync) o;
  if (System.identityHashCode(this)<System.identityHashCode(o))
  {
    synchronized (this)
    {
      synchronized (o)
      {
         return equalsHelper(o);
      }
    }
  }
  else
  {
    synchronized (o)
    {
      synchronized (this)
      {
        return equalsHelper(o);
      }
    }
  }
}

然后将 equalsHelper 声明为私有并让它做真正的比较工作。

(但是哇,对于这样一个微不足道的问题,有很多代码。)

请注意,要使其正常工作,任何可以更改对象状态的函数都必须声明为同步。

另一种选择是在 Sync.class 上同步,而不是在任何一个对象上同步,然后也同步 Sync.class 上的任何设置器。这会将所有内容锁定在单个互斥锁上并避免整个问题。当然,根据您正在执行的操作,这可能会导致某些线程出现意外阻塞。您必须根据您的程序的含义来考虑其含义。

如果这是您正在处理的项目中的一个真正问题,那么需要考虑的一个重要替代方案是使对象不可变。想想 String 和 StringBuilder。您可以创建一个 SyncBuilder 对象,让您可以执行创建其中一项所需的任何工作,然后拥有一个 Sync 对象,其状态由构造函数设置并且永远不会改变。创建一个构造函数,该构造函数采用 SyncBuilder 并将其状态设置为匹配或具有 SyncBuilder.toSync 方法。无论哪种方式,您都在 SyncBuilder 中完成所有构建,然后将其转换为 Sync,现在您可以保证不变性,因此您根本不必弄乱同步。

于 2009-10-28T13:21:05.517 回答
-2

对变量的读取和写入int已经是原子的,因此无需同步 getter 和 setter(请参阅http://java.sun.com/docs/books/tutorial/essential/concurrency/atomic.html)。

同样,您不需要在equals此处同步。虽然您可以阻止另一个线程i在比较期间更改其中一个值,但该线程将简单地阻塞,直到equals方法完成并在之后立即更改它。

于 2009-10-28T12:07:19.077 回答