2013-07-08 1 views
1

Почему этот код не является потокобезопасным, хотя мы используем синхронизированный метод и, следовательно, получаем блокировку на объекте Helper?Почему этот код не является потокобезопасным, даже при использовании синхронизированного метода?

class ListHelper <E> { 
    public List<E> list = Collections.synchronizedList(new ArrayList<E>()); 

    public synchronized boolean putIfAbsent(E x) { 
     boolean absent = !list.contains(x); 
     if (absent) 
      list.add(x); 
     return absent; 
    } 
} 
+4

Возможно, это ... Кто сказал, что это не потолочный сейф? Получили ли вы состояние гонки или тупик? –

+0

Это был пример в Java Concurrency In Practice, который не мог понять, почему этот пример был указан как небезопасный. – Ullas

+0

Я немного удивлен, что они не объяснили, почему ... –

ответ

11

Поскольку список разблокирован, когда contains возвращается, а затем снова заблокирован, когда вызывается add. Что-то еще может добавить один и тот же элемент между ними.

Если вы хотите использовать список только из вспомогательного объекта, он должен быть объявлен private; если вы это сделаете, код будет потокобезопасным, если все манипуляции с списком проходят через методы, которые синхронизируются в вспомогательном объекте. Также стоит отметить, что до тех пор, пока это так, вам не нужно использовать Collections.synchronizedList, поскольку вы предоставляете всю необходимую синхронизацию в своем собственном коде.

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

class ListHelper <E> { 
    public List<E> list = Collections.synchronizedList(new ArrayList<E>()); 

    public boolean putIfAbsent(E x) { 
     synchronized (list) { 
      boolean absent = !list.contains(x); 
      if (absent) 
       list.add(x); 
      return absent; 
     } 
    } 
} 

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

+0

Точно, я согласен. –

+0

Спасибо, в соответствии с объяснением в JCP, он будет терпеть неудачу, потому что мы используем неправильную блокировку (т. Е. Используя вспомогательный объект как блокировку). Сохраняя список как общедоступный, и если мы используем синхронизированный блок, используя список как метод блокировки для вызова, он будет работать нормально. Я пропустил публичную часть во время чтения, спасибо, что указал на это. – Ullas

3

Этот код не является потокобезопасным только потому, что он является общедоступным.

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

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

Когда вы отмечаете синхронизированный метод, все потоки, вызывающие этот метод, синхронизируются с экземпляром объекта, указанный метод указан. Поэтому, если внутренний экземпляр списка ListHelper не упоминается в другом месте, и все методы синхронизированы, ваш код будет быть потокобезопасным.

1

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

В примере проводке:

public synchronized boolean putIfAbsent(E x) { 
    boolean absent = !list.contains(x); 
    if (absent) 
     list.add(x); 
    return absent; 
} 

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

Если вы принимаете элементы этого списка из кода клиента, которому вы не доверяете или не знаете, хорошей практикой было бы сделать защитную копию x, а затем добавить ее в свой список. Аналогичным образом, если вы вернете объект из своего списка в другой код клиента, создайте защитную копию и верните ее, чтобы убедиться, что ваш список остается потокобезопасным.

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

+0

Хороший момент. При рассмотрении вопросов такого рода я склонен предположить, что используемые объекты являются неизменными, но всегда стоит убедиться, что либо это так, либо были предприняты шаги для решения проблем, вызванных, если они не являются. – Jules

Смежные вопросы