J'essaie d'ajouter des valeurs de String
à un ArrayList
en utilisant deux threads. Ce que je veux, c'est que pendant qu'un thread ajoute les valeurs, l'autre thread ne doit pas interférer, j'ai donc utilisé la méthode Collections.synchronizedList
. Mais il semble que si je ne synchronise pas explicitement sur un objet, l'ajout se fait de manière non synchronisée.
Sans bloc synchronisé explicite:
public class SynTest {
public static void main(String []args){
final List<String> list=new ArrayList<String>();
final List<String> synList=Collections.synchronizedList(list);
final Object o=new Object();
Thread tOne=new Thread(new Runnable(){
@Override
public void run() {
//synchronized(o){
for(int i=0;i<100;i++){
System.out.println(synList.add("add one"+i)+ " one");
}
//}
}
});
Thread tTwo=new Thread(new Runnable(){
@Override
public void run() {
//synchronized(o){
for(int i=0;i<100;i++){
System.out.println(synList.add("add two"+i)+" two");
}
//}
}
});
tOne.start();
tTwo.start();
}
}
La sortie que j'ai obtenue est:
true one
true two
true one
true two
true one
true two
true two
true one
true one
true one...
Avec le bloc synchronisé explicite non commenté, j'arrête l'interférence de l'autre thread lors de l'ajout. Une fois que le thread a acquis le verrou, il s'exécute jusqu'à ce qu'il soit terminé.
exemple de sortie après décommentation du bloc synchronisé:
true one
true one
true one
true one
true one
true one
true one
true one...
Alors pourquoi la Collections.synchronizedList()
ne fait pas la synchronisation?
Une liste synchronisée synchronise uniquement les méthodes de cette liste.
Cela signifie qu'un thread ne pourra pas modifier la liste alors qu'un autre thread exécute actuellement une méthode de cette liste. L'objet est verrouillé lors du traitement de la méthode.
Par exemple, supposons que deux threads exécutent addAll
sur votre liste, avec 2 listes différentes (A=A1,A2,A3
et B=B1,B2,B3
) comme paramètre.
Comme la méthode est synchronisée, vous pouvez être sûr que ces listes ne seront pas fusionnées de manière aléatoire comme A1,B1,A2,A3,B2,B3
Vous ne décidez pas quand un thread passe le processus à l'autre thread. Chaque appel de méthode doit être entièrement exécuté et renvoyé avant que l'autre ne puisse s'exécuter. Vous pouvez donc obtenir A1,A2,A3,B1,B2,B3
ou B1,B2,B3,A1,A2,A3
(Comme nous ne savons pas quel appel de thread s'exécutera en premier).
Dans votre premier morceau de code, les deux threads s'exécutent en même temps. Et les deux essaient de add
un élément de la liste. Vous n'avez aucun moyen de bloquer un thread sauf la synchronisation sur la méthode add
donc rien n'empêche le thread 1 d'exécuter plusieurs opérations add
avant de passer le processus au thread 2. Ainsi, votre sortie est parfaitement normal.
Dans votre deuxième morceau de code (celui qui n'est pas commenté), vous indiquez clairement qu'un thread verrouille complètement la liste de l'autre thread avant de démarrer la boucle. Par conséquent, vous vous assurez que l'un de vos threads exécutera la boucle complète avant que l'autre ne puisse accéder à la liste.
Collections.synchronizedList()
synchronisera tous les accès à la liste sauvegardée sauf pendant l'itération qui doit encore être effectuée dans un bloc synchronisé avec l'instance de liste synchronisée comme moniteur d'objet.
Voici par exemple le code de la méthode add
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
Cela garantit un accès série à la liste sauvegardée, donc si vos 2 threads appellent add
en même temps, un thread acquerra le verrou, ajoutera son élément et relâchera le verrou, puis le deuxième thread pourra acquérir le verrouiller et ajouter son élément c'est pourquoi vous obtenez alternativement one
et two
dans votre sortie.
Lorsque vous décommentez le bloc synchronisé, le code est alors
synchronized(o) {
for(int i=0;i<100;i++){
...
}
}
Dans ce cas, le thread qui pourrait acquérir le verrou sur o
exécutera d'abord la boucle entierfor
avant de libérer le verrou (sauf si une exception est levée), permettant l'autre thread pour exécuter le contenu de son bloc synchronisé, c'est pourquoi vous obtenez 100
fois consécutives one
ou two
puis 100
fois consécutives l'autre valeur.
Le comportement observable est absolument correct - l'approche synchronized
que vous démontrez dans l'exemple de code n'est pas la même que synchronizedList
. Dans le premier cas, vous synchronisez l'intégralité de l'instruction for, donc un seul thread l'exécutera simultanément. Dans le deuxième cas, vous synchronisez les méthodes de collecte elles-mêmes - c'est ce que signifie synchronizedList
. Assurez-vous donc que la méthode add
est synchronisée - mais pas la méthode for
!
Selon les réponses précédentes, vous devez synchroniser le synList
du thread d'accès tOne
et tTwo
. Dans ce cas, vous pouvez utiliser le modèle de moniteur pour fournir un accès de sécurité - pour les threads.
Ci-dessous, j'ai adapté votre code pour le partager avec d'autres personnes ayant les mêmes problèmes. Dans ce code, j'ai utilisé uniquement le synList
pour contrôler l'accès de manière synchronisée. Notez qu'il n'est pas nécessaire de créer un autre objet pour garantir l'accès à la commande à partir de synList. Pour compléter cette question, voir le livre Java Concurrence in Practice jcip chapitre 4 qui parle des modèles de conception de moniteur qui est inspiré par le travail de Hoare
public class SynTest {
public static void main(String []args){
final List<String> synList= Collections.synchronizedList(new ArrayList<>());
Thread tOne=new Thread(() -> {
synchronized (synList) {
for (int i = 0; i < 100; i++) {
System.out.println(synList.add("add one" + i) + " one");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread tTwo=new Thread(()->{
synchronized (synList) {
for(int i=0;i<100;i++){
System.out.println(synList.add("add two"+i)+" two");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
tOne.start();
tTwo.start();
}
}