Sécurité des threads lors d’une itération dans une liste de tableaux à l’aide de foreach

J’ai une liste de ArrayList qui est instanciée et remplie sur le thread d’arrière-plan (je l’utilise pour stocker les données du Cursor ). Dans le même temps, il peut être consulté sur le thread principal et itéré à l’aide de foreach. Cela peut donc évidemment entraîner une exception.

Ma question est la suivante: quelle est la meilleure pratique pour rendre cette classe fil-safe sans la copier à chaque fois ou en utilisant des indicateurs?

 class SomeClass { private final Context mContext; private List 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 général, il est très mauvais d’opérer simultanément sur une structure de données qui n’est pas thread-safe. Vous n’avez aucune garantie que l’implémentation ne changera pas à l’avenir, ce qui pourrait avoir de graves conséquences sur le comportement au moment de l’exécution de l’application, c’est-à-dire que java.util.HashMap provoque des boucles infinies lors de la modification simultanée.

Pour accéder simultanément à une liste, Java fournit java.util.concurrent.CopyOnWriteArrayList . L’utilisation de cette implémentation résoudra votre problème de différentes manières:

  • il est thread-safe, permettant des modifications simultanées
  • itérer sur des instantanés de la liste n’est pas affecté par les opérations d’ajout simultanées, ce qui permet d’append et d’itérer simultanément
  • c’est plus rapide que la synchronisation

Sinon, si ne pas utiliser une copie du tableau interne est une exigence ssortingcte (ce que je ne peux pas imaginer dans votre cas, le tableau est plutôt petit car il ne contient que des références d’object, qui peuvent être copiées en mémoire assez efficacement), vous pouvez synchroniser les access sur la carte. Mais cela nécessiterait que la mappe soit initialisée correctement, sinon votre code pourrait NullPointerException une NullPointerException car l’ordre d’exécution du thread n’est pas garanti (vous supposez que la fonction populateList() été démarrée. La liste est donc initialisée. Lors de l’utilisation d’un bloc , choisissez judicieusement le bloc protégé. Si vous avez tout le contenu de la méthode run() dans un bloc synchronisé, le thread lecteur doit attendre que les résultats du curseur soient traités – ce qui peut prendre un certain temps – de sorte que vous perdez réellement tout concurence.

Si vous décidez de choisir le bloc synchronisé, j’apporterai les modifications suivantes (et je ne prétends pas, elles sont parfaitement correctes):

Initialisez le champ de liste afin que nous puissions synchroniser l’access sur celui-ci:

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

Synchronisez l’opération de modification (add). Ne lisez pas les données du curseur à l’intérieur du bloc de synchronisation car, s’il s’agit d’une opération à faible temps de latence, la commande mList ne peut pas être lue pendant cette opération, ce qui bloque tous les autres threads pendant un certain temps.

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

L’itération de lecture doit se trouver à l’intérieur du bloc de synchronisation afin qu’aucun élément ne soit ajouté lors de l’itération:

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

Ainsi, lorsque deux threads opèrent dans la liste, un thread peut append un seul élément ou effectuer une itération sur toute la liste à la fois. Vous n’avez pas d’exécution parallèle sur ces parties du code.

Concernant les versions synchronisées d’une liste (c’est-à-dire Vector , Collections.synchronizedList() ). Celles-ci sont peut-être moins performantes car avec la synchronisation, vous perdez en fait une exécution préliminaire car un seul thread peut exécuter les blocs protégés à la fois. En outre, ils peuvent toujours être sujets à une ConcurrentModificationException , qui peut même se produire dans un seul thread. Il est levé si la structure de données est modifiée entre la création d’iterator et iterator doit continuer. Donc, ces infrastructures de données ne résoudront pas votre problème.

Je ne recommande pas non plus la synchronisation manuelle, car le risque de ne pas le faire correctement est trop élevé (synchronisation sur un moniteur différent ou incorrect, blocs de synchronisation trop grands, …)

TL; DR

utiliser un java.util.concurrent.CopyOnWriteArrayList

Utilisez Collections.synchronizedList(new ArrayList());

Ex:

 Collections.synchronizedList(mList); 

bloc synchronisé java http://www.tutorialspoint.com/java/java_thread_synchronization.htm

 class SomeClass { private final Context mContext; private List 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; } } } 

Vous pouvez utiliser un Vector qui est l’équivalent thread-safe de ArrayList .

EDIT: Grâce au commentaire de Fildor , je sais maintenant que cela n’empêche pas ConcurrentModificationException d’être émise à l’aide de plusieurs threads:

Seuls les appels simples seront synchronisés. Ainsi, un ajout ne peut pas être appelé pendant qu’un autre thread appelle un ajout, par exemple. Mais la modification de la liste entraînera le lancement de la CME lors de son itération sur un autre thread. Vous pouvez lire la documentation de l’iterator sur ce sujet.

Aussi intéressant:

  • Pourquoi la classe Java Vector est-elle considérée comme obsolète ou obsolète?
  • Vector vs Collections.synchronizedList (ArrayList)

Longue histoire courte: NE PAS utiliser Vector .