Nous avons plusieurs threads appelant add(obj)
sur un ArrayList
.
Ma théorie est que lorsque add
est appelé simultanément par deux threads, seul un des deux objets ajoutés est réellement ajouté au ArrayList
. Est-ce plausable?
Si oui, comment contournez-vous cela? Utiliser une collection synchronisée comme Vector
?
Il n'y a aucun comportement garanti pour ce qui se passe lorsque add est appelé simultanément par deux threads sur ArrayList. Cependant, d'après mon expérience, les deux objets ont bien été ajoutés. La plupart des problèmes de sécurité des threads liés aux listes traitent de l'itération lors de l'ajout/de la suppression. Malgré cela, je déconseille fortement d'utiliser Vanilla ArrayList avec plusieurs threads et un accès simultané.
Le vecteur était la norme pour les listes simultanées, mais maintenant la norme consiste à utiliser la Liste synchronisée des collections .
Aussi, je recommande fortement Java Concurrence in Practice by Goetz et al si vous allez passer du temps à travailler avec des threads en Java. Le livre couvre ce problème avec beaucoup plus de détails.
Un certain nombre de choses pourraient se produire. Vous pourriez obtenir les deux objets ajoutés correctement. Vous ne pouvez ajouter qu'un seul des objets. Vous pouvez obtenir une exception ArrayIndexOutOfBounds car la taille du tableau sous-jacent n'a pas été ajustée correctement. Ou d'autres choses peuvent arriver. Il suffit de dire que vous ne pouvez pas compter sur un comportement quelconque.
Comme alternatives, vous pouvez utiliser Vector
, vous pouvez utiliser Collections.synchronizedList
, vous pouvez utiliser CopyOnWriteArrayList
, ou vous pouvez utiliser un verrou séparé. Tout dépend de ce que vous faites d'autre et du type de contrôle que vous avez sur l'accès à la collection.
Vous pouvez également obtenir un null
, un ArrayOutOfBoundsException
, ou quelque chose à laisser à l'implémentation. HashMap
s ont été observés pour entrer dans une boucle infinie dans les systèmes de production. Vous n'avez pas vraiment besoin de savoir ce qui pourrait mal se passer, mais ne le faites pas.
Vous pouvez utiliser Vector
, mais cela a tendance à fonctionner, l'interface n'est pas assez riche. Vous constaterez probablement que vous voulez une structure de données différente dans la plupart des cas.
J'ai trouvé le code suivant pour imiter quelque peu un scénario du monde réel.
100 tâches sont exécutées en parallèle et mettent à jour leur état terminé dans le programme principal. J'utilise un CountDownLatch pour attendre la fin de la tâche.
import Java.util.concurrent.*;
import Java.util.*;
public class Runner {
// Should be replaced with Collections.synchronizedList(new ArrayList<Integer>())
public List<Integer> completed = new ArrayList<Integer>();
/**
* @param args
*/
public static void main(String[] args) {
Runner r = new Runner();
ExecutorService exe = Executors.newFixedThreadPool(30);
int tasks = 100;
CountDownLatch latch = new CountDownLatch(tasks);
for (int i = 0; i < tasks; i++) {
exe.submit(r.new Task(i, latch));
}
try {
latch.await();
System.out.println("Summary:");
System.out.println("Number of tasks completed: "
+ r.completed.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
exe.shutdown();
}
class Task implements Runnable {
private int id;
private CountDownLatch latch;
public Task(int id, CountDownLatch latch) {
this.id = id;
this.latch = latch;
}
public void run() {
Random r = new Random();
try {
Thread.sleep(r.nextInt(5000)); //Actual work of the task
} catch (InterruptedException e) {
e.printStackTrace();
}
completed.add(id);
latch.countDown();
}
}
}
Lorsque j'ai exécuté l'application 10 fois et au moins 3 à 4 fois, le programme n'a pas imprimé le nombre correct de tâches terminées. Idéalement, il devrait imprimer 100 (si aucune exception ne se produit). Mais dans certains cas, il imprimait 98, 99, etc.
Ainsi, cela prouve que les mises à jour simultanées d'ArrayList ne donneront pas de résultats corrects.
Si je remplace la ArrayList par une version Synchronized, le programme sort les résultats corrects.
vous pouvez utiliser List l = Collections.synchronizedList(new ArrayList());
si vous voulez une version thread-safe de arrayList.
Le comportement n'est probablement pas défini car ArrayList n'est pas threadsafe. Si vous modifiez la liste alors qu'un itérateur interagit dessus, vous obtiendrez une exception ConcurrentModificationException. Vous pouvez encapsuler ArrayList avec Collection.synchronizedList ou utiliser une collection thread-safe (il y en a beaucoup), ou simplement placer les appels d'ajout dans un bloc synchronisé.
Java.util.concurrent a une liste de tableaux thread-safe. La liste ArrayList standard n'est pas adaptée aux threads et le comportement lorsque plusieurs threads sont mis à jour en même temps n'est pas défini. Il peut également y avoir des comportements étranges avec plusieurs lecteurs lorsqu'un ou plusieurs threads écrivent en même temps.
Vous pouvez utiliser à la place de ArrayList();
:
Collections.synchronizedList( new ArrayList() );
ou
new Vector();
synchronizedList
selon moi préférable car c'est:
http://Java.Sun.com/j2se/1.4.2/docs/api/Java/util/ArrayList.html
Notez que cette implémentation n'est pas synchronisée. Si plusieurs threads accèdent simultanément à une instance ArrayList et qu'au moins l'un des threads modifie la liste structurellement, elle doit être synchronisée en externe.
Puisqu'il n'y a pas de synchronisation interne, ce que vous théorisez n'est pas plausible.
Ainsi, les choses se désynchronisent, avec des résultats désagréables et imprévisibles.