代码之家  ›  专栏  ›  技术社区  ›  Philipp

在Java中正确同步均衡器()

  •  10
  • Philipp  · 技术社区  · 15 年前

    我有一个只包含一个字段的类 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;
        }
    }
    
    14 回复  |  直到 13 年前
        1
  •  9
  •   leonm    15 年前

    正在尝试同步 equals hashCode 对象内部将无法正常工作。考虑一个 HashMap 使用的 哈希码 发现对象将在哪个“bucket”中,然后使用 等于 按顺序搜索存储桶中的所有对象。

    如果允许对象以改变 哈希码 等于 你可能会在一个场景中 哈希图 电话 哈希码 . 它获取锁,获取散列并再次释放锁。 哈希图 然后继续计算要使用哪个“bucket”。但以前 哈希图 可以在上获取锁,等于其他人获取锁并对对象进行变异,以便 等于 与以前的值不一致 哈希码 . 这将导致灾难性的后果。

    这个 哈希码 在很多地方都使用方法,并且是Java集合API的核心。我可能有必要重新考虑不需要同步访问这些方法的应用程序结构。或者至少不同步对象本身。

        2
  •  7
  •   mmmmmm    15 年前

    为什么要同步?在这种情况下,重要的是它们在比较过程中是否发生了变化,而取决于相等性的代码在运行之前是否立即发生了变化。(即如果您有依赖于相等性的代码,那么如果在此代码之前或期间值变得不相等,会发生什么情况)

    我想你得看看更大的过程,看看你需要锁在哪里。

        3
  •  6
  •   sfussenegger    15 年前

    其中,如果在保留同步后不能保证结果为真,则synchronizing 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?
        }
      }
    }
    

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

        4
  •  3
  •   Peter Lawrey    15 年前

    如果说得够多的话,hashcode()、equals()或compareto()使用的字段应该是不可变的,最好是final。在这种情况下,您不需要同步它们。

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

        5
  •  2
  •   user58804    15 年前

    您试图在可变对象上定义基于内容的“equals”和“hashcode”。这不仅不可能,而且毫无意义。根据

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

    “equals”和“hashcode”都需要一致:对于同一对象上的连续调用,返回相同的值。根据定义,易变性阻止了这一点。这不仅仅是理论:许多其他类(例如集合)依赖于实现equals/hashcode正确语义的对象。

    同步问题是个棘手的问题。当您解决底层问题(易变性)时,不需要同步。如果不解决易变性问题,那么再多的同步也帮不了你。

        6
  •  1
  •   joel.neely    15 年前

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

    不能阻止两个线程调用 set …方法的任意顺序。所以即使一个线程得到一个(有效的) true 从呼叫 .equals( ) ,该结果可能会立即被另一个调用 设置 …在其中一个物体上。这个结果只意味着在比较的时候这些值是相等的。

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

        7
  •  1
  •   Stephen C    15 年前

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

    如果你把两个都锁上 this 以及 other 对象的同时,确实存在死锁的风险。但我怀疑你需要这么做。相反,我认为你应该 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();
    }
    

    这不能保证两个对象具有相同的值 与此同时,但这不太可能产生任何实际影响。毕竟,即使你有这样的保证,你仍然需要处理这样一个问题:当 等于 回电话了。(这是@s的观点!)

    此外,这并不能完全消除僵局的风险。考虑线程可以调用 等于 在两个物体中的一个上拿着锁;例如。

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

    如果两个线程调用 a.frobbitt(b) b.frobbitt(a) 分别存在僵局的风险。

    (不过,你确实需要打电话给 getI() 或宣布 成为 volatile ,否则 equals() 可以看到 如果它是最近由另一个线程更新的。)

    有人说过,基于价值的 等于 对象上的方法,其组件值可能会发生变化。例如,这将破坏许多集合类型。将此与多线程结合起来,您将很难确定您的代码是否真的正确。我忍不住想你最好换个 等于 hashcode 方法,使它们不依赖于在第一次调用方法后可能发生变化的状态。

        8
  •  1
  •   Stephen Denne    15 年前

    始终以相同的顺序锁定它们,一种可以确定顺序的方法是 System.identityHashCode(Object)

    编辑以包含注释:

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

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

    您可以创建一个共享实用程序来在锁需求的短期内跟踪具有相同标识hashcode的对象,并在跟踪期间为它们提供可重复的顺序。

        9
  •  0
  •   skaffman    15 年前

    正确实施 equals() hashCode() 是散列数据结构之类的各种事情所必需的,因此您没有实际的选择。从另一个角度来看, 等式() 哈希德() 只是方法,对同步的要求与其他方法相同。您仍然有死锁问题,但这并不是特定于 等式() 这就是原因。

        10
  •  0
  •   Jay    15 年前

    正如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 private并让它做真正的比较工作。

    (但哇,这是一个很小问题的代码。)

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

    另一个选项是同步sync.class而不是同步任何一个对象,然后同步sync.class上的任何setter。这将把所有东西都锁定在一个互斥锁上,从而避免整个问题。当然,这取决于您正在执行的操作,可能会导致某些线程被意外阻塞。你必须根据你的程序是关于什么来考虑它的含义。

    如果这是您正在处理的项目中的一个实际问题,则需要考虑的另一个重要选择是使对象不可变。想想string和stringbuilder。你可以创建一个syncbuilder对象,它允许你做任何你需要做的工作来创建一个syncbuilder对象,然后有一个syncbuilder对象,它的状态由构造函数设置,并且永远不会改变。创建采用SyncBuilder并将其状态设置为匹配或具有SyncBuilder.ToSync方法的构造函数。无论哪种方式,您都可以在syncbuilder中完成所有构建,然后将其转换为同步,现在您可以保证不变性,这样就完全不必干扰同步。

        11
  •  0
  •   Serge Bogatyrev    15 年前

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

        12
  •  0
  •   robinr    13 年前

    您需要确保对象不会在对hashcode()和equals()的调用(如果调用)之间发生更改。然后,当对象位于hashmap中时,必须确保对象不会发生更改(以扩展hashcode和equals)。要更改对象,必须先将其删除,然后更改并将其放回。

        13
  •  0
  •   jtahlborn    13 年前

    正如其他人所提到的,如果在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;
    }
    
        14
  •  -2
  •   Jason Day    15 年前

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

    同样,您不需要同步 equals 在这里。而你可以阻止另一个线程更改 i 在比较过程中,该线程将简单地阻塞,直到 等于 方法完成后立即更改它。