Как можно избежать java.util.ConcurrentModificationException при использовании OSC?

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

Программа написана на Java с использованием Processing и OSC. Основной обработчик событий OSC добавляет элементы в вектор. Он срабатывает при вводе данных пользователем и поэтому крайне непредсказуем. Этот вектор также повторяется и обновляется в потоке анимации Processing, который происходит очень регулярно, примерно 60 раз в секунду.

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

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

Что я могу сделать, чтобы предотвратить такое поведение? Есть ли способ получить доступ к Вектору только в том случае, если он еще не используется?

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

Вот некоторый псевдокод:

Vector<String> list = new Vector<String>();
Vector<Particle> completedParticles = new Vector<Particle>();

public void oscEvent( OSCMessage message )
{
    list.add( new Particle( message.x, message.y ) );
}

public void draw()
{
    completedParticles.clear();
    for( Particle p : list )
    {
        p.draw();
        if( p.isComplete ) {
            completedParticles.add( p );
        }   
    }
    list.removeAll( completedParticles );
}

person JeremyFromEarth    schedule 03.07.2012    source источник
comment
вы смотрели в CopyOnWriteArrayList или ConcurrentHashMap? любой из них должен помочь избежать исключения ConcurrentModificationException   -  person ali haider    schedule 04.07.2012
comment
Ваш цикл for перебирает список, и ваш osEvent изменяет список. Два потока, работающих одновременно, могут пытаться выполнить итерацию по списку, в то время как другой добавляет к нему элементы. Ваш for создает итератор.   -  person Edwin Dalorzo    schedule 04.07.2012
comment
Да, я вижу проблему. Я не уверен, как этого избежать.   -  person JeremyFromEarth    schedule 04.07.2012
comment
synchronized(list) { /* здесь ваш цикл */ } ‹-- Таким образом, вы сохраняете блокировку, а list.add(...), вызываемый другим потоком, будет блокироваться до завершения итерации   -  person MartinK    schedule 04.07.2012
comment
@jeremynealbrown Предположим, что это единственные два случая, вы можете синхронизировать доступ к списку, создав методы syncrhonized или используя synchronized(list).   -  person Edwin Dalorzo    schedule 04.07.2012
comment
спустя три года эти вопросы и его ответы просто спасли мне жизнь :)   -  person Sr.Richie    schedule 26.04.2016


Ответы (4)


О вашем коде

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

Вы можете сделать следующее (при условии, что это единственные два места, где это происходит):

//osEvent
synchronized(this.list) {
   list.add( new Particle( message.x, message.y ) );
}

//draw
synchronized(this.list) {
  for( Particle p : list )
    {
        p.draw();
        if( p.isComplete ) {
            completedParticles.add( p );
        }   
    }
}

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

Об исключении одновременного изменения

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

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

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

Позвольте мне привести несколько примеров:

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

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

Соберите все записи, которые вы хотите удалить, в расширенном цикле for, и после завершения итерации вы удалите все найденные записи.

ISBN isbn = new ISBN("0-201-63361-2");
List<Book> found = new ArrayList<Book>();
for(Book book : books){
    if(book.getIsbn().equals(isbn)){
        found.add(book);
    }
}
books.removeAll(found);

Или вы можете использовать ListIterator, который поддерживает метод удаления/добавления во время самой итерации.

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext()){
    if(iter.next().getIsbn().equals(isbn)){
        iter.remove();
    }
}

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

synchronized(this.books) {
   List<Book> copyOfBooks = new ArrayList<Book>(this.books)
}
for(Book book : copyOfBooks) {
   System.out.println(book);
}

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

person Edwin Dalorzo    schedule 03.07.2012
comment
Спасибо за ваш ответ. На самом деле я делаю именно то, что вы рекомендуете во втором примере кода, где элементы удаляются после итерации. Возможно, что-то добавляется в список во время его повторения. Однако, насколько я могу судить, это произойдет только в результате параллельной процедуры. - person JeremyFromEarth; 04.07.2012
comment
@jeremynealbrown Тогда синхронизация должна быть нарушена где-то в вашей программе. В какой-то момент ваша коллекция модифицируется, в то время как где-то еще повторяется. Учтите, что большинство итераторов коллекций поддерживаются исходной коллекцией и что циклы for-each используют итераторы за кулисами. - person Edwin Dalorzo; 04.07.2012
comment
Обертывание итерационных процессов в синхронизированные блоки, похоже, достаточно хорошо исправило ситуацию. Это не только решило проблему, но я также научился некоторым действительно полезным приемам, которые я уверен, что буду использовать снова. Спасибо всем за помощь.!! - person JeremyFromEarth; 04.07.2012

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

java.util.concurrent.locks.Lock lock = new java.util.concurrent.locks.ReentrantLock();
Vector<String> list = new Vector<String>();
Vector<Particle> completedParticles = new Vector<Particle>();

public void oscEvent( OSCMessage message )
{
    lock.lock();
    try {
      list.add( new Particle( message.x, message.y ) );
    } finally {
      lock.unlock();
    }
}

public void draw()
{
    completedParticles.clear();
    lock.lock();
    try {
      for( Particle p : list )
      {
          p.draw();
          if( p.isComplete ) {
              completedParticles.add( p );
          }   
      }
      list.removeAll( completedParticles );
    } finally {
      lock.unlock();
    }
}
person Affe    schedule 03.07.2012

Это произойдет, если элементы будут добавлены/удалены из коллекции во время итерации. Некоторые вещи, на которые вы можете обратить внимание:

  1. CopyOnWriteArrayList() - довольно дорого, но может помочь вам избежать исключения одновременной модификации, или
  2. Использовать concurrentHashMap
  3. Синхронизировать на самой итерации
person ali haider    schedule 03.07.2012

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

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

// example only
public class LockingVector {
  private final Vector v;
  private final ReentrantLock lock = new ReentrantLock();

  public void lock(){
    lock.lock();
  }
  public void unlock(){
    lock.unlock();
  }

  // other 'vector' method delegated to v
  public Object get() {
    return v.get();
  }
}

Затем использовать это сделать что-то вроде

 public class Main {
   public static void main(String[] args){
     Vector v = ...
     LockingVector lv = new LockingVector(v);
     try {
       lv.lock();
       // do stuff here (add, delete, iterate, etc.)
     } finally {
       lv.unlock();
     }
   }
 }
person cyber-monk    schedule 03.07.2012