2009-10-28 2 views
10

У меня есть следующий класс, который содержит только одно поле i. Доступ к этому полю защищен блокировкой объекта («это»). При реализации equals() мне нужно заблокировать этот экземпляр (a) и другой (b). Если поток 1 вызывает a.equals (b), и в то же время поток 2 вызывает b.equals (a), порядок блокировки является обратным в двух реализациях и может привести к тупиковой ситуации.Правильно синхронизировать equals() в Java

Как реализовать 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; 
    } 
} 

ответ

0

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

7

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

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

+0

Вы правы, что одна величина может измениться сразу после сравнения, но так как когда обернутое значение более интересно, чем примитивное 'int', существует вероятность того, что это значение находится в противоречивом состоянии, если не синхронизировано, пока его части проверяются. –

+1

Правда, но у вас все еще есть проблема, что делать после равных – Mark

+2

на самом деле, проблема одна из видимости. без использования синхронизации, действительное упорядочение может по-прежнему приводить к неожиданному результату (т. е. ранее установленное значение не отображается, когда оно должно быть). – jtahlborn

1

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

Edit включить комментарий:

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

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

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

+0

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; boolean lessHash = System.identityHashCode (this) Philipp

+0

'System.identityHashCode' не дает уникальных значений! –

9

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

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

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

+0

Почему downvote ?? Если вы посмотрите на классы в JDK (java.util.Date или java.lang.Long), вы обнаружите, что они также не синхронизированы. – leonm

+1

«Если объектам разрешено мутировать, что изменяет результаты hashCode или равно ...» Большинство объектов - даже java.util.Date (setTime (long)). Это всегда необходимо учитывать при использовании хешированной структуры данных - не меняйте ее после того, как вы ее испортили. Тем не менее, это разрешено изменить. – sfussenegger

6

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

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? 
    } 
    } 
} 

Правда, вы по-прежнему сталкиваются с той же проблемой, но, по крайней мере, ваш синхронизации в нужный момент;)

1

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

+0

> Кроме того, это не полностью устраняет риск тупика. Рассмотрим случай, когда поток может называть равным, удерживая блокировку на одном из двух объектов. Я не уверен, как это проблема. Если это блокировка этого, никакая проблема, поскольку getI() будет реентерабельной и не повлияет на other.getI(). То же самое, если это чужой замок. Только для справки о том, как это делают другие: Вектор синхронизируется на этом, но не на другом в equals(). Это может привести к возникновению ConcurrentModificationException, если вектор изменен, когда выполняется равенство(). AtomicInteger не реализует equals() – Philipp

+0

@Philipp - вам понадобится два потока, которые делают это с одной и той же парой объектов в противоположных положениях, чтобы получить тупик. –

+0

Не выйдет ли this.getI() его блокировка до вызова other.getI()? Я не понимаю, как это может зайти в тупик. – Philipp

1

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

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

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

-2

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

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

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, и теперь вам гарантирована неизменность, поэтому вам вообще не нужно возиться с синхронизацией.

3

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

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

+1

Как и вычисления для ArrayList? ;-) Шутка в сторону, непреложный финал - это хорошая вещь, к которой нужно стремиться (возможно, за пределами хорошей вещи - все намного проще, когда вы используете неизменяемые объекты), но это не всегда возможно с большими составными объектами. –

+0

Как вы используете hashCode() с ArrayList? Хотя невозможно сделать объекты неизменяемыми, это не изменяет того факта, что вы не можете изменить hashCode() ключа карты или элементов набора и заставить его работать. –

0

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

2

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

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

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

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

+3

Изменяемые объекты из JVM do реализуют equals (пример: ArrayList, HashMap), поэтому ваше утверждение, что «контент-основанный» равно «(...) на изменяемом объекте (...) не имеет смысла», не разделяется теми, кто пишет JVM. Даже некоторые sychronized mutable классы реализуют equals (пример Vector, который не является безопасным для взаимоблокировки). – Philipp

+1

Это хороший момент. Тем не менее, я бы сказал, что ответственность вызывающего абонента заключается в том, чтобы сделать эти объекты эффективно неизменными в течение любой операции, которая использует equals/hashCode. Например, вызов List .contains (x) при изменении x или мутация ключей в Map Имеет неопределенное поведение. – user58804

0

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

0

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

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; 
} 
Смежные вопросы