Seguridad de subprocesos al realizar iteraciones a través de un ArrayList utilizando foreach

Tengo un ArrayList que está siendo instanciado y poblado en el hilo de fondo (lo uso para almacenar los datos del Cursor ). Al mismo tiempo se puede acceder en el hilo principal e iterado mediante el uso de foreach. Así que esto obviamente puede resultar en lanzar una excepción.

Mi pregunta es ¿cuál es la mejor práctica para hacer que este campo de clase sea seguro para el hilo sin copiarlo cada vez o usar banderas?

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

En general, es una muy mala idea operar simultáneamente en una estructura de datos que no es thread-safe. No tiene ninguna garantía de que la implementación no cambiará en el futuro, lo que puede afectar gravemente el comportamiento en tiempo de ejecución de la aplicación, es decir, java.util.HashMap produce bucles infinitos al ser modificado simultáneamente.

Para acceder a una lista simultáneamente, Java proporciona java.util.concurrent.CopyOnWriteArrayList . El uso de esta implementación resolverá su problema de varias maneras:

  • Es thread-safe, permitiendo modificaciones simultáneas
  • La iteración sobre instantáneas de la lista no se ve afectada por las operaciones de adición concurrentes, permitiendo simultáneamente agregar e iterar
  • Es más rápido que la sincronización

Alternativamente, si no se utiliza una copia de la matriz interna es un requisito estricto (que no puedo imaginar en su caso, la matriz es bastante pequeña, ya que sólo contiene referencias de objetos, que se pueden copiar en la memoria con bastante eficiencia), puede Sincronizar el acceso en el mapa. Pero eso requeriría que el mapa se inicializara correctamente, de lo contrario su código puede lanzar una NullPointerException porque el orden de ejecución de subprocesos no está garantizado (asume que el populateList() se inicia antes, por lo que la lista se inicializa.Cuando se utiliza un bloque sincronizado , Elija el bloque protegido sabiamente.Si tiene todo el contenido del método run() en un bloque sincronizado, el hilo del lector tiene que esperar hasta que los resultados del cursor se procesen – lo que puede tomar un tiempo – por lo que realmente suelto Toda concurrencia.

Si usted decide ir para el bloque sincronizado, haría los cambios siguientes (y no lo reclamo, ellos son perfectamente correctos):

Inicialice el campo de lista para poder sincronizar el acceso en él:

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

Sincronizar la operación de modificación (add). No lea los datos del cursor dentro del bloque de sincronización porque si es una operación de baja latencia, el mList no se pudo leer durante esa operación, bloqueando todos los otros hilos durante bastante tiempo.

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

La iteración de lectura debe estar dentro del bloque de sincronización, por lo que no se agrega ningún elemento, mientras se itera sobre:

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

Así que cuando dos hilos funcionan en la lista, un hilo puede agregar un solo elemento o iterar sobre la lista completa a la vez. No tiene ejecución paralela en estas partes del código.

Respecto a las versiones sincronizadas de una lista (es decir Vector , Collections.synchronizedList() ). Éstos pueden ser menos eficientes porque con la sincronización realmente pierdes la ejecución paralela ya que sólo un hilo puede ejecutar los bloques protegidos a la vez. Además, pueden seguir siendo propensos a ConcurrentModificationException , que puede incluso ocurrir en un solo hilo. Se lanza, si la estructura de datos se modifica entre la creación de iteradores y el iterador debe continuar. Así que esas estructuras de datos no resolverán su problema.

No recomiendo la sincronización manual también, porque el riesgo de simplemente hacerla mal es demasiado alto (sincronización en el monitory incorrecto o diferente, bloques de sincronización demasiado grandes, …)

TL, DR

Utilice java.util.concurrent.CopyOnWriteArrayList

Utilice Collections.synchronizedList(new ArrayList<T>());

Ex:

 Collections.synchronizedList(mList); 

Java bloque sincronizado 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; } } } 

Puede utilizar un Vector que sea el equivalente a ArrayList de thread-safe.

EDIT: Gracias al comentario de Fildor , ahora sé que esto no evita que ConcurrentModificationException se lance con varios subprocesos:

Sólo se sincronizarán las llamadas individuales. Por lo tanto, una adición no se puede llamar mientras otro subproceso está llamando add, por ejemplo. Pero alterar la lista hará que el CME se lance mientras itera en otro hilo. Puede leer los documentos de iterador sobre ese tema.

También interesante:

  • ¿Por qué la clase Java Vector se considera obsoleta o obsoleta?
  • Vector vs Collections.synchronizedList (ArrayList)

Larga historia corta: NO utilice Vector .

  • Ejecutar mensajes del controlador en un subproceso de fondo
  • Bloqueo de hilo principal de Android que bloquea hilo de WebView
  • Android Render y Física en hilos separados?
  • AsyncTask hilo sigue allí después de ejecutar, ¿es normal?
  • Sincronizar varias ventanas con varias vistas en Android
  • Threading de ContentProvider
  • Manejo de InterruptedException mientras espera una señal de salida (error en Android?)
  • ¿Cuál es la diferencia entre un hilo y un manejador
  • Hacer solicitud de volley en diferentes hilos
  • Carga pesada en el hilo que causa problemas de memoria
  • ¿Puede TCriticalSection.Acquire ser llamado con seguridad más de una vez por un hilo?
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.