ConcurrentModificationException, несмотря на использование синхронизированного

 public synchronized X getAnotherX(){ 
  if(iterator.hasNext()){
   X b = iterator.next();
   String name = b.getInputFileName();
  ...
   return b;
  }
  else{return null;}
 }

несмотря на синхронизированный оператор в заголовке объявления, я все равно получаю исключение ConcurrentModificationException в строке, где я использую iterator.next(); Что здесь не так?

Ответы

Ответ 1

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

Iterator iterator = collection.iterator();
while (iterator.hasNext()) {
    Item item = (Item) iterator.next();
    if (item.satisfiesCondition()) {
       collection.remove(item);
    }
}

В этом случае вы должны использовать метод iterator.remove(). Это происходит одинаково, если вы добавляете в коллекцию, и в этом случае нет общего решения. Тем не менее, подтип ListIterator может использоваться при работе со списком и имеет метод add().

Ответ 2

Я согласен с вышеприведенными утверждениями о ConcurrentModificationException, которые часто происходят в результате изменения коллекции в том же потоке, что и итерация. Однако это не всегда причина.

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

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

synchronized (foo) {
  foo.setBar();
}

И вы можете думать, что у вас есть эксклюзивный доступ к нему. Тем не менее, нет ничего, чтобы остановить другой поток, просто делая что-то без блока synchronized:

foo.setBar();  // No synchronization first.

Через неудачу (или Murphy Law, "Все, что может пойти не так, пойдет не так".), эти два потока могут произойти для выполнения в одно и то же время. В случае структурных модификаций некоторых широко используемых коллекций (например, ArrayList, HashSet, HashMap и т.д.) Это может привести к ConcurrentModificationException.

Трудно полностью предотвратить проблему:

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

    Существует аннотация javax.annotation.concurrent.GuardedBy, которая может помочь документировать это стандартным образом; проблема в том, что вы должны иметь некоторые средства для проверки правильного использования аннотации в toolchain. Например, вы могли бы использовать что-то вроде Google errorprone, который может проверять в некоторых ситуациях, но он не идеален.

  • Для простых операций над коллекциями вы можете использовать методы Collections.synchronizedXXX factory, которые обертывают коллекцию так что каждый вызов метода сначала синхронизируется в базовой коллекции, например метод SynchronizedCollection.add:

    @Override public boolean add(E e) {
      synchronized (mutex) { return c.add(obj); }
    }
    

    Где mutex - это синхронизированный экземпляр (часто сам SynchronizedCollection), а c - обернутая коллекция.

    Два оговорки с таким подходом:

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

      Collections.synchronizedList(new ArrayList<T>());
      
    • Синхронизация применяется к вызову метода, поэтому, если вы выполняете некоторую сложную операцию, например

      if (c.size() > 5) { c.add(new Frob()); }
      

      тогда у вас нет эксклюзивного доступа во всей этой операции, только для вызовов size() и add(...) индивидуально.

      Чтобы получить взаимоисключающий доступ в течение продолжительности сложной операции, вам необходимо выполнить внешнюю синхронизацию, например. synchronized (c) { ... }. Это требует, чтобы вы знали правильную вещь для синхронизации, однако, которая может быть или не быть c.