Проблема с синхронизированными коллекциями Java при выполнении equals () в обратном порядке из нескольких потоков - PullRequest
3 голосов
/ 12 октября 2009

Пример сценария :

  • Создание двух синхронизированных наборов (s1 и s2)
  • Передайте их двум потокам (T1 и T2)
  • Запустить темы

T1's run (): в то время как (навсегда) s1.equals (s2)

T2's run (): в то время как (навсегда) s2.equals (s1)

Что происходит? - равные SynchronizedSet получает блокировку на себя

  • Он вычисляет длину передаваемого параметра, а также то, что он содержит, чтобы определить, равен ли он [Примечание: это предположение на основе проанализированных мною журналов]

  • Если переданный параметр также является SynchronizedSet, вызовы size () и containsAll () подразумевают блокировку того, что также должно быть получено.

  • В приведенном выше примере заказы на блокировку для T1 и T2 выглядят следующим образом:

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

Ofc, это приводит к тупику.

Эта проблема не относится только к синхронизированным коллекциям. Это может случиться даже с Hashtable или Vector.

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

Ответы [ 5 ]

4 голосов
/ 12 октября 2009

Я считаю, что это ограничение API Java (дизайн).

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

На самом деле, я думаю, что единственным способом избежать этого было бы либо:

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

Оба эти подхода нецелесообразны и не поддаются масштабированию.

Как это побороть?

Кодируйте ваше приложение, чтобы блокировки были получены всеми потоками в одном и том же порядке. Ответы @ Мориса и @ Неттогрофа дают примеры того, как это сделать, хотя это может быть сложнее, если у вас есть множество сетов, о которых нужно беспокоиться.

2 голосов
/ 12 октября 2009

Я могу предложить использовать блок synchronized () {].

как то так:

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

и

while(forever){
   synchronized(s1){
    s2.equals(s1);
   }
}
2 голосов
/ 12 октября 2009

Вы можете заблокировать оба набора в одном и том же порядке в каждом потоке:

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

и

            synchronized(s1) {
                synchronized(s2) {
                    s2.equals(s1);
                }
            }
1 голос
/ 12 октября 2009

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

1 голос
/ 12 октября 2009

Стивен С. Хорошие вещи. Дополнительная информация: Если вы не знаете, как обстоят дела с наборами, вы можете использовать «глобальную» блокировку всякий раз, когда оба набора будут сравниваться:

 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);

Иногда вы можете использовать альтернативный алгоритм. IIRC, StringBuffer может зайти в тупик с добавлением (хотя комбинация операций над объектами не имеет большого смысла). Это может быть реализовано как:

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

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

...