Soluzione
Esercizio 006 - Test and Set
Quale
era il difetto riscontrato nel settembre del 1999 da un mio amico nella
seguente proposta di realizzazione di Test-and-Set in Java?
final
class TestAndSet {
private
boolean busy;
TestAndSet(boolean
initialBusy){
busy = initialBusy;
}
TestAndSet(){
this(true);
}
synchronized
void releaseResource(){
busy = false;
try {notify();} catch (Throwable t) {}
}
synchronized
void lockResourceWaiting(){
while (busy)
try {wait();} catch (Throwable t) {}
busy = true;
}
synchronized
boolean testAndLockResource(){
if (busy) // already locked
return false; // by another
else {
busy = true;
return true; // locked by me
}
}
}
Ebbene
la precedente realizzazione java fa esattamente quello che deve fare: realizza
cioè con successo una primitiva di Test-and-Set! Il vero problema
è che questa implementazione è soggetta ad un attacco DOS
(Denial of Service).
Se
non sapete cos'è un attacco DOS, oppure volete semplicemente rinfrescarvi
la memoria, potete consultare due ottime pagine in rete: quella
del CERT del Software Engineering Institute della Carnegie Mellon,
oppure la Denial of Service (DoS)
Attack Resources che contiene un buon insieme di link (e anche una
simpatica vignetta introduttiva...).
Ma
torniamo a noi: qual'è il problema nel codice qui sopra? Il problema
è dovuto al fatto che un qualsiasi altro thread della mia applicazione
Java può bloccare l'intera applicazione (o per lo meno quella parte
applicativa che usa TestAndSet)
con un semplice metodo tipo il seguente:
void
BlockTestAndSet(TestAndSet ts){
synchronized(ts) {
while (true)
;
}
Infatti
durante l'esecuzione (infinita!) di BlockTestAndSet
chiunque altro provi ad usare un qualunque metodo della TestAndSet
su quella istanza rimane bloccato per sempre. Il problema è dovuto
al fatto che TestAndSet
offre dei metodi pubblici synchronized
sullo stesso oggetto TestAndSet.
Per ovviare a questo problema è sufficiente riscrivere TestAndSet
nel seguente modo:
final
class TestAndSet{
private boolean busy;
private Object sem;
TestAndSet(boolean initialBusy){
busy = initialBusy;
sem = new Object();
}
TestAndSet(){
this(true);
}
void releaseResource(){
synchronized (sem) {
busy = false;
try { notify();} catch (Throwable t) {}
}
}
void lockResourceWaiting(){
synchronized (sem) {
while (busy)
try { wait();} catch (Throwable t) {}
busy = true;
}
}
boolean testAndLockResource(){
synchronized (sem) {
if (busy)
return false; // already locked by another
else {
busy = true;
return true; // locked by me
}
}
}
}
In
questa seconda implementazione il semaforo su cui si fa synchronized
è un oggetto privato di TestAndSet
e quindi non è accessibile all'esterno e quindi non è lockabile
da un altro thread.
Abbiamo
quindi imparato come nascondere i dettagli implementativi all'interno della
nostra implementazione e lasciare visibile all'esterno solo i metodi non
synchronized
effettivamente necessari.
Non
abbiamo comunque completamente risolto il problema: un hacker-thread può
sempre, dopo aver ottenuto il controllo della risorsa, non rilasciarla
mai, non chiamando mai la releaseResource.
In questo modo gli altri threads che usino la lockResourceWaiting
rimarranno per sempre bloccati. Ma anche in questo caso c'è una
scappatoia: se invece gli altri threads usano la testAndLockResource
possono decidere cosa fare se trovano la risorsa occupata per troppo tempo!
Ricordiamo
comunque sempre che, come recita il paragrafo
finale della sezione 17.13 della specifica di Java:
The
Java programming language does not prevent, nor require detection of, deadlock
conditions. Programs where threads hold (directly or indirectly) locks
on multiple objects should use conventional techniques for deadlock avoidance,
creating higher-level locking primitives that don't deadlock, if necessary.