Правильная синхронизация equals() в Java

У меня есть следующий класс, который содержит только одно поле 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;
    }
}

Ответ 1

Попытка выполнить синхронизацию equals и hashCode внутри объекта будет работать неправильно. Рассмотрим случай HashMap, который использует hashCode для обнаружения того, в каком "ведре" будет находиться объект, а затем использует equals для последовательного поиска всех объектов в ведре.

Если объектам разрешено мутировать таким образом, чтобы изменить результаты hashCode или equals, вы можете получить сценарий, в котором HashMap вызывает hashCode. Он приобретает замок, получает хэш и снова освобождает блокировку. HashMap затем перейдет к вычислению того, какой "ковш" использовать. Но прежде чем HashMap сможет получить блокировку на равных, кто-то еще захватит блокировку и мутирует объект, чтобы equals стал несовместимым с предыдущим значением hashCode. Это приведет к катастрофическим результатам.

Методы hashCode и equals используются во многих местах и ​​являются основой для API коллекций Java. Мне может быть полезно переосмыслить структуру вашего приложения, которая не требует синхронного доступа к этим методам. Или, по крайней мере, не синхронизироваться с самим объектом.

Ответ 2

Зачем синхронизировать? Каков прецедент, когда это важно, если они меняются во время сравнения, и не имеет значения, если это произойдет сразу же после того, как код будет зависеть от выполнения равенства. (т.е. если у вас есть код в зависимости от эффективности, что произойдет, если значения становятся неравными до или во время этого кода)

Я думаю, вам нужно взглянуть на более крупный процесс, чтобы увидеть, где вам нужно блокировать.

Ответ 3

Где находится точка синхронизации 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

Если было сказано достаточно, поля, которые вы используете для hashCode(), equals() или compareTo(), должны быть неизменными, предпочтительно конечными. В этом случае вам не нужно их синхронизировать.

Единственная причина для реализации hashCode() заключается в том, что объект может быть добавлен в хеш-коллекцию, и вы не можете корректно изменить hashCode() объекта, который был добавлен в такую ​​коллекцию.

Ответ 5

Вы пытаетесь определить контентные "equals" и "hashCode" на изменяемом объекте. Это не только невозможно: это не имеет смысла. Согласно

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

как "equals", так и "hashCode" должны быть последовательными: вернуть одно и то же значение для последовательных вызовов на один и тот же объект (ы). Помехаемость по определению предотвращает это. Это не просто теория: многие другие классы (например, коллекции) зависят от объектов, реализующих правильную семантику для equals/hashCode.

Проблема синхронизации - это красная селедка. Когда вы решаете основную проблему (изменчивость), вам не нужно синхронизировать. Если вы не решите проблему мутации, никакая синхронизация вам не поможет.

Ответ 6

Всегда блокируйте их в том же порядке, так, как вы могли бы решить, что порядок указан по результатам System.identityHashCode(Object)

Изменить для включения комментария:

Лучшее решение для решения редкого случая идентичности идентификатораHashCodes требует более подробной информации о том, что происходит с другой блокировкой этих объектов.

Все требования к блокировке объектов должны использовать один и тот же процесс разрешения.

Вы можете создать общую утилиту для отслеживания объектов с одинаковым идентификаторомHashCode для короткого периода требований к блокировке и обеспечить повторное упорядочение для них в течение периода, в котором они отслеживаются.

Ответ 7

Единственный способ убедиться, что синхронизация строго необходима, - это анализировать всю программу для ситуаций. Есть две вещи, которые вам нужно искать; ситуации, когда один поток меняет объект, а другой - на равный, и ситуации, когда поток, вызывающий 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();
}

Это не гарантирует, что оба объекта имеют одно и то же значение i одновременно, но это вряд ли имеет практическое значение. В конце концов, даже если у вас есть эта гарантия, вам все равно придется справиться с проблемой, что два объекта могут не быть равными к моменту возврата вызова equals. (Это точка @s!)

Кроме того, это не полностью устраняет риск тупика. Рассмотрим случай, когда поток может вызывать equals, удерживая блокировку на одном из двух объектов; например.

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

Теперь, если два потока обращаются к a.frobbitt(b) и b.frobbitt(a) соответственно, существует опасность блокировки.

(Тем не менее, вам нужно вызвать getI() или объявить i как volatile, иначе equals() может видеть устаревшее значение i, если оно было недавно обновлено другим потоком.)

Это, как было сказано, что-то вызывает беспокойство в отношении метода equals, основанного на значении, на объекте, значения компонентов которого могут быть изменены. Например, это нарушит многие типы коллекций. Объедините это с многопоточным, и вам будет сложно понять, действительно ли ваш код правильный. Я не могу не думать о том, что вам лучше изменить методы equals и hashcode, чтобы они не зависели от состояния, которое может мутировать после того, как методы были вызваны в первый раз.

Ответ 8

(Я предполагаю, что вас интересует общий случай здесь, а не только в завернутых целых числах.)

Вы не можете запретить двум потокам вызывать методы set... в произвольном порядке. Поэтому даже когда один поток получает (действительный) true от вызова .equals(... ), этот результат может быть немедленно аннулирован другим потоком, который вызывает set... на одном из объектов. IOW результат только означает, что значения были равны в момент сравнения.

Следовательно, синхронизация защитит от случая, когда обернутое значение находится в несогласованном состоянии, пока вы пытаетесь выполнить сравнение (например, две int -размерные половинки обернутого long обновляются последовательно). Вы можете избежать состояния гонки, скопировав каждое значение (то есть независимо синхронизировавшись, без перекрытия), а затем сравнив копии.

Ответ 9

Правильная реализация equals() и hashCode() требуется для различных вещей, таких как структуры хэширования данных, и поэтому у вас нет реальной возможности. С другой стороны, equals() и hashCode() - это просто методы с теми же требованиями к синхронизации, что и другие методы. У вас все еще есть проблема взаимоблокировки, но это не связано с тем, что она equals() вызывает ее.

Ответ 10

Как указывает Джейсон Дей, целые сравнения уже являются атомарными, поэтому синхронизация здесь является излишней. Но если вы просто строили упрощенный пример и в реальной жизни вы думаете о более сложном объекте:

Прямой ответ на ваш вопрос: убедитесь, что вы всегда сравниваете элементы в последовательном порядке. Неважно, что это за заказ, если он последователен. В таком случае 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 и дайте ему выполнить реальную работу по сравнению.

(Но ничего себе, много кода для такой тривиальной проблемы.)

Обратите внимание, что для этой работы любая функция, которая может изменить состояние объекта, должна быть объявлена ​​синхронизированной.

Другим вариантом будет синхронизация на Sync.class, а не на любом объекте, а затем синхронизация любых сеттеров на Sync.class. Это заблокировало бы все на одном мьютексе и избегало всей проблемы. Конечно, в зависимости от того, что вы делаете, это может вызвать нежелательную блокировку некоторых потоков. Вам придется подумать о последствиях в свете того, о чем идет речь.

Если это реальная проблема в проекте, над которым вы работаете, серьезной альтернативой для рассмотрения было бы сделать объект неизменным. Подумайте о String и StringBuilder. Вы можете создать объект SyncBuilder, который позволяет выполнять любую работу, необходимую для создания одной из этих вещей, а затем иметь объект Sync, состояние которого задается конструктором и не может измениться. Создайте конструктор, который принимает SyncBuilder и устанавливает его состояние для соответствия или имеет метод SyncBuilder.toSync. В любом случае, вы делаете все свое здание в SyncBuilder, а затем превращаете его в Sync, и теперь вам гарантирована неизменность, поэтому вам вообще не нужно возиться с синхронизацией.

Ответ 11

Не используйте синхронизацию. Подумайте о немодифицируемом beans.

Ответ 12

Вам нужно убедиться, что объекты не меняются между вызовами hashCode() и equals() (если они вызываются). Затем вы должны заверить, что объекты не меняются (в зависимости от того, что hashCode и равно), в то время как объект находится в хэш-карте. Чтобы изменить объект, вы должны сначала удалить его, затем изменить и вернуть его.

Ответ 13

Как отмечали другие, если вещи меняются во время проверки равенства, уже существует возможность сумасшедшего поведения (даже при правильной синхронизации). так что все, о чем вам действительно нужно беспокоиться, - это видимость (вы хотите убедиться, что изменение, которое "происходит до того, как" будет видно ваш равный вызов). поэтому вы можете просто сделать "моментальный снимок" равным, что будет правильным с точки зрения отношения "произойдет до" и не будет страдать от проблем с упорядочением блокировки:

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

Чтение и запись в int переменные уже являются атомарными, поэтому нет необходимости синхронизировать приемник и сеттер (см. http://java.sun.com/docs/books/tutorial/essential/concurrency/atomic.html).

Аналогично, вам не нужно синхронизировать equals здесь. Хотя вы могли бы запретить другому потоку изменять один из значений i во время сравнения, этот поток просто блокируется до тех пор, пока метод equals не завершится и не изменит его сразу после этого.