Stavo cercando di implementare qualcosa di simile all'interfaccia BlockingQueue limitata di Java usando "primitive" di sincronizzazione Java (sincronizzato, wait(), notify()) quando mi sono imbattuto in qualche comportamento che non capisco.Java: due WAITING + un thread BLOCCATO, notify() porta a un livelock, notifyAll() no, perché?
Creo una coda in grado di memorizzare 1 elemento, creare due thread che attendono il recupero di un valore dalla coda, avviarli, quindi provare a inserire due valori nella coda in un blocco sincronizzato nel thread principale. La maggior parte delle volte funziona, ma a volte i due thread in attesa di un valore iniziano a svegliarsi apparentemente l'un l'altro e non lasciare che il thread principale entri nel blocco sincronizzato.
Ecco il mio codice (semplificato):
import java.util.LinkedList;
import java.util.Queue;
public class LivelockDemo {
private static final int MANY_RUNS = 10000;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < MANY_RUNS; i++) { // to increase the probability
final MyBoundedBlockingQueue ctr = new MyBoundedBlockingQueue(1);
Thread t1 = createObserver(ctr, i + ":1");
Thread t2 = createObserver(ctr, i + ":2");
t1.start();
t2.start();
System.out.println(i + ":0 ready to enter synchronized block");
synchronized (ctr) {
System.out.println(i + ":0 entered synchronized block");
ctr.addWhenHasSpace("hello");
ctr.addWhenHasSpace("world");
}
t1.join();
t2.join();
System.out.println();
}
}
public static class MyBoundedBlockingQueue {
private Queue<Object> lst = new LinkedList<Object>();;
private int limit;
private MyBoundedBlockingQueue(int limit) {
this.limit = limit;
}
public synchronized void addWhenHasSpace(Object obj) throws InterruptedException {
boolean printed = false;
while (lst.size() >= limit) {
printed = __heartbeat(':', printed);
notify();
wait();
}
lst.offer(obj);
notify();
}
// waits until something has been set and then returns it
public synchronized Object getWhenNotEmpty() throws InterruptedException {
boolean printed = false;
while (lst.isEmpty()) {
printed = __heartbeat('.', printed); // show progress
notify();
wait();
}
Object result = lst.poll();
notify();
return result;
}
// just to show progress of waiting threads in a reasonable manner
private static boolean __heartbeat(char c, boolean printed) {
long now = System.currentTimeMillis();
if (now % 1000 == 0) {
System.out.print(c);
printed = true;
} else if (printed) {
System.out.println();
printed = false;
}
return printed;
}
}
private static Thread createObserver(final MyBoundedBlockingQueue ctr,
final String name) {
return new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(name + ": saw " + ctr.getWhenNotEmpty());
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}
}, name);
}
}
Ecco quello che vedo quando si "blocchi":
(skipped a lot)
85:0 ready to enter synchronized block
85:0 entered synchronized block
85:2: saw hello
85:1: saw world
86:0 ready to enter synchronized block
86:0 entered synchronized block
86:2: saw hello
86:1: saw world
87:0 ready to enter synchronized block
............................................
..........................................................................
..................................................................................
(goes "forever")
Tuttavia, se cambio la notify() chiama all'interno del while (. ..) loop di addWhenHasSpace e getWhenNotEmpty metodi per notifyAll(), passa "sempre".
La mia domanda è questa: perché il comportamento varia tra i metodi notify() e notifyAll() in questo caso, e anche perché il comportamento di notify() è così com'è?
mi aspetterei entrambi i metodi si comportino allo stesso modo, in questo caso (due thread in attesa, uno bloccato), perché:
- mi sembra che in questo caso notifyAll() sarebbe svegliarsi solo fino l'altro thread, come notify();
- sembra che la scelta del metodo che risveglia un thread influenzi il modo in cui il thread viene risvegliato (e diventa RUNNABLE suppongo) e il thread principale (che è stato BLOCCATO) più tardi competere per il blocco - non qualcosa che mi aspetterei da javadoc così come la ricerca su internet sull'argomento.
O forse sto facendo qualcosa di sbagliato del tutto?
Perché stai chiamando 'notify()' * e * 'wait()' in un ciclo? È possibile che tu voglia due monitor: uno per "c'è qualcosa da consumare" e un altro per "c'è uno spazio da riempire". –
Grazie, mi hai fatto capire che stavo facendo una cosa stupida costantemente svegliando i fili mentre aspettavo che * nessuno * di loro non fosse cambiato. Mi sono fissato sul problema e non ho visto un (ovvio) modo migliore. Mi viene ancora da chiedersi perché notify() e notifyAll() si comportino diversamente in questo caso, ma poiché c'è un modo molto migliore per farlo, questa domanda è meramente di interesse teorico. – starikoff