2016-08-09 2 views
6

У меня есть ArrayList, который создается и заполняется фоновым потоком (я использую его для хранения данных Cursor). В то же время к нему можно получить доступ к основному потоку и выполнить итерацию с помощью foreach. Таким образом, это, очевидно, может привести к исключению.Безопасность нитей при повторении через ArrayList с использованием foreach

Мой вопрос в том, что лучше всего сделать это поле класса потокобезопасным без копирования его каждый раз или с использованием флагов?

class SomeClass { 

    private final Context mContext; 
    private List<String> mList = null; 

    SomeClass(Context context) { 
     mContext = context; 
    } 

    public void populateList() { 
     new Thread(new Runnable() { 
      @Override 
      public void run() { 
       mList = new ArrayList<>(); 

       Cursor cursor = mContext.getContentResolver().query(
         DataProvider.CONTENT_URI, null, null, null, null); 
       try { 
        while (cursor.moveToNext()) { 
         mList.add(cursor.getString(cursor.getColumnIndex(DataProvider.NAME))); 
        } 
       } catch (Exception e) { 
        Log.e("Error", e.getMessage(), e); 
       } finally { 
        if (cursor != null) { 
         cursor.close(); 
        } 
       } 
      } 
     }).start(); 
    } 

    public boolean searchList(String query) { // Invoked on the main thread 
     if (mList != null) { 
      for (String name : mList) { 
       if (name.equals(query) { 
        return true; 
       } 
      } 
     } 

     return false; 
    } 
} 

ответ

3

В общем, это очень плохая идея, чтобы работать одновременно на структура данных, не поточно- , У вас нет гарантии, что реализация не изменится в будущем, что может серьезно повлиять на поведение среды выполнения приложения, то есть java.util.HashMap вызывает бесконечные циклы при одновременном изменении.

Для доступа к списку одновременно Java предоставляет java.util.concurrent.CopyOnWriteArrayList.С помощью этой реализации будет решить вашу проблему различных способов:

  • это потокобезопасно, что позволяет одновременно модификации
  • итерации снимки списка не зависят от параллельных операций оных, что позволяет одновременно добавлять и перебор
  • это быстрее, чем синхронизация

в качестве альтернативы, если не используя копию внутреннего массива является строгое требование (что я не могу себе представить в вашем случае, массив довольно мал, поскольку он содержит только ссылки на объекты, которые можно скопировать в памяти довольно эффективно), вы можете синхронизировать доступ на карте. Но для этого требуется, чтобы карта была правильно инициализирована, в противном случае ваш код может вызывать NullPointerException, потому что порядок выполнения потока не гарантируется (вы предполагаете, что populateList() запущен раньше, поэтому список инициализируется. При использовании синхронизированного блока , выберите защищенный блок с умом. Если у вас есть весь контент метода run() в синхронизированном блоке, поток чтения должен ждать, пока обрабатываются результаты от курсора, что может занять некоторое время, - поэтому вы фактически теряете весь параллелизм .

Если вы решили пойти на синхронизированный блок, я хотел бы сделать следующие изменения (и я не утверждаю, что они совершенно правы):

Инициализировать поле списка таким образом мы можем синхронизировать доступ на него:

private List<String> mList = new ArrayList<>(); //initialize the field 

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

//mList = new ArrayList<>(); remove that line in your code 
String data = cursor.getString(cursor.getColumnIndex(DataProvider.NAME)); //do this before synchronized block! 
synchronized(mList){ 
    mList.add(data); 
} 

Считанные итерации должен быть внутри блока синхронизации, так что ни один элемент не будет добавлен, в то же время итерации:

synchronized(mList){ 
    for (String name : mList) { 
    if (name.equals(query) { 
     return true; 
    } 
    } 
} 

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

Относительно синхронизированных версий списка (то есть Vector, Collections.synchronizedList()). Они могут быть менее эффективными, поскольку при синхронизации вы фактически теряете параллельное выполнение, так как только один поток может запускать защищенные блоки за раз. Кроме того, они все еще могут быть подвержены ConcurrentModificationException, что может происходить даже в одном потоке. Он бросается, если структура данных изменяется между созданием итератора и итератором. Таким образом, эти структуры не решают вашу проблему.

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

TL; DR

использовать java.util.concurrent.CopyOnWriteArrayList

+0

Я бы предпочел также 'CopyOnWriteArrayList', но OP сказал« без копирования ». Может быть, он хочет гарантировать, что «только один поток может запускать защищенные блоки за раз». – beatngu13

+2

«не копирование» -реквизиции своего рода бессмысленно. Конечно, CopyOnWriteArrayList * * копирует содержимое, но это часть деталей реализации. Единственной альтернативой может быть синхронизированный блок, синхронизирующий на самой карте, который подвержен ошибкам, поскольку он инициализируется символом «null». Использование 'Vector' не предотвращает' ConcurrentModificationException', потому что оно не имеет ничего общего с параллелизмом (оно вызывается, когда итератор пытается продолжить, но список был изменен после создания итератора) –

+0

Спасибо за объяснение. Я думаю, что 'CopyOnWriteArrayList' лучше для моего случая (так как я обращаюсь к полю в потоке пользовательского интерфейса). – Nikolai

1

Вы можете использовать Vector, которая является поточно-эквивалент ArrayList.

EDIT: Благодаря Fildor's comment, теперь я знаю, что это не позволяет избежать ConcurrentModificationException от броска с помощью Множественные темы:

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

Также интересно:

Короче говоря: НЕ использование Vector.

+0

Просто с помощью Vector не избежать получения ConcurrentModificationException. – Fildor

+0

@Fildor Но только если тот же поток пытается итерации и изменения, или я здесь не так? Я думал, что синхронизация препятствует одновременному доступу нескольких потоков к структуре данных. – beatngu13

+2

Да, но это не имеет никакого отношения к CME. Синхронизируются только одиночные вызовы. Таким образом, одно добавление нельзя вызвать, в то время как другой поток вызывает добавление, например. Но изменение списка приведет к выбросу CME при повторении в другом потоке. Вы можете прочитать документы итератора по этой теме. Я сам вошел в эту ловушку - я узнал от боли;) – Fildor

1

Использование Collections.synchronizedList(new ArrayList<T>());

Ex:

Collections.synchronizedList(mList); 
+0

Не поможет избежать одновременной модификации. – Fildor

1

Java синхронизируются блок http://www.tutorialspoint.com/java/java_thread_synchronization.htm

class SomeClass { 

    private final Context mContext; 
    private List<String> mList = null; 

    SomeClass(Context context) { 
     mContext = context; 
    } 

    public void populateList() { 
     new Thread(new Runnable() { 
      @Override 
      public void run() { 
       synchronized(SomeClass.this){ 
        mList = new ArrayList<>(); 

        Cursor cursor = mContext.getContentResolver().query(
          DataProvider.CONTENT_URI, null, null, null, null); 
        try { 
         while (cursor.moveToNext()) { 
          mList.add(cursor.getString(cursor.getColumnIndex(DataProvider.NAME))); 
         } 
        } catch (Exception e) { 
         Log.e("Error", e.getMessage(), e); 
        } finally { 
         if (cursor != null) { 
          cursor.close(); 
         } 
        } 
       } 
      } 
     }).start(); 
    } 

    public boolean searchList(String query) { // Invoked on the main thread 
    synchronized(SomeClass.this){ 
      if (mList != null) { 
       for (String name : mList) { 
        if (name.equals(query) { 
         return true; 
        } 
       } 
      } 

      return false; 
     } 
    } 
} 
+0

Вы уверены, синхронизированные блоки синхронизируются на одном мониторе? –

+0

Вы правы на runnable, должно быть точное указание на класс. Этот объект теперь (после обновления). Я уверен, что он должен работать –

+1

и положить весь метод run() в синхронизированный блок будет блокировать доступ в течение довольно длительного времени ... предположим, что вы перебираете более 1000 элементов, каждый из которых требует 1 секунду для восстановления, приложение не отвечает за ~ 16min –

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