MokaByte
Numero 17 - Marzo 1998
|
|||
|
|||
Piergiuseppe Spinelli |
|
||
Java è
un linguaggio semplice in quanto permette una gestione automatica della
memoria grazie al garbage collector. In questo articolo vedremo cos'è
e come funzioni l'incapsulazione ed il mascheramento del codice offerti
da Java utilissimi strumenti, specialmente in fase di prototipizzazione.
Studiando
l'implementazione delle classi standard è tuttavia possibile apportare
ottimizzazioni spesso decisive al nostro codice.
Una vasta libreria
standard come quella del JDK ha il valore dell'oro puro quando si tratta
di implementare velocemente dei prototipi o di consegnare nei tempi prestabiliti
(vale a dire ieri) la prima versione funzionante di un modulo software.
Troppo spesso
però il prototipo viene promosso sul campo a sistema finale o, al
contrario, un componente di sistema è bocciato per problemi di performances
senza valutare che la sostituzione di un paio di classi standard con altre
ottimizzate ad hoc potrebbe ridare la carica al codice apparentemente pigro
ed inaffidabile.
Vediamo come
sia possibile avvalersi al massimo delle classi della libreria JDK superando
eventuali ostacoli dovuti alla loro implementazione e traendo spunto proprio
da quest'ultima per giungere ad una soluzione ottimale.
Java ci viene
incontro in questo bisogno di conoscenza mettendo a nostra disposizione
i sorgenti commentati di tutte le classi standard di sistema. Questo è
senza dubbio un buon punto d'inizio.
Prenderemo come esempio
un problema in cui mi sono imbattuto portando in Java un sistema d'acquisizione
dati e sperimenteremo una tecnica ispirata ad un articolo apparso su JavaWorld,
dal titolo: "How to decouple the Observer/Observable object model" .
Pattern
Observable/Observer: l'implementazione Java
Java fornisce varie classi
standard che implementano alcuni pattern d'uso frequente. Uno di questi è
il pattern Observable/Observer che ha un vasto campo d'applicazione ogniqualvolta
una sorgente di informazione (programma di calcolo, trigger di una porta di
comunicazione, gestore di eventi di input, etc.) debba tenere aggiornati un
certo numero di client sui propri mutamenti di stato.
Consideriamo un modulo d'acquisizione dati di processo da una linea di produzione industriale. Esso potrebbe, ad esempio, leggere ad intervalli regolari i contatori dei pezzi scartati divisi per causale di scarto e mantenuti nei controllori programmabili della macchina. Quando i valori letti differiscono da quelli precedenti, il modulo d'acquisizione notifica l'evento a tutti gli oggetti del sistema che abbiano dichiarato interesse per tale informazione.
Nell'esempio, ll modulo d'acquisizione è un oggetto Observable mentre tutti i fruitori dell'informazione sono degli Observer registrati su di esso. La potenza di questo pattern è data dal fatto che l'oggetto osservato non deve avere alcuna conoscenza del numero e del tipo dei propri osservatori, e nemmeno del loro scopo. Tipici fruitori dei dati sugli scarti macchina possono essere:
Altro
punto di forza è l'estrema semplicità d'uso del modello.
Il codice Java relativo all'esempio suona più o meno così:
Si noti che Observer è un'interfaccia mentre Observable è una classe astratta. Ciò potrebbe imporre qualche limitazione a causa dell'ereditarietà semplice di Java (ad esempio AcquisisciDati deve implementare l'interfaccia Runnable non potendo ereditare direttamente dalla classe Thread) ma questo, di solito, è un problema minore.
Dalla
teoria alla pratica, chi avesse rilasciato la versione finale del proprio
sistema d'acquisizione dati seguendo l'esempio precedente andrebbe sicuramente
incontro a spiacevoli sorprese. Facilmente, alla faccia della trasparenza
degli osservatori, il nostro modulo d'acquisizione potrebbe andare in crash
con l'introduzione di un nuovo client oppure funzionare a dovere con tutti
ma non quando sono contemporaneamente attivi, e così via senza metter
limite alla fantasia.
Il
problema del coupling stretto tra Observable ed Observer
Prima di inoltrarsci
nel cuore del problema, diamo uno sguardo ai sorgenti della classe Observable,
ed in particolare al metodo notifyObservers():
public void notifyObservers(Object arg) {La classe Observable non ha nessuna conoscenza del comportamento dei metodi update() dei vari oggetti registrati e, in particolare, non può fare nessuna previsione sulla loro durata d'esecuzione. Per di più, considerando che un'istanza di Observer potrebbe generare errori durante l'esecuzione, esistono due possibilità:
int size=0;
synchronized (this) {
if (!hasChanged()) return;
size = obs.size();
if (size > arr.length) arr = new Observer[size];
obs.copyInto(arr);
clearChanged();
}
for (int i = size -1; i>=0; i--) {
if (arr[i] != null) arr[i].update(this, arg);
}
}
Questo tipo di problemi di accoppiamento (coupling) tra parti di codice era già conosciuto ben prima dell'affermazione dei sistemi ad oggetti. Il listato di notifyObserver() ci conferma come i metodi update() dei vari osservatori vengano eseguiti a carico del tempo e delle risorse del thread che esegue l'oggetto osservato.
Implementazione
di ThObservable
Il codice di
ThObservable, quindi, è fondamentalmente una modifica della
classe standard Observable con l'aggiunta di una coda di eventi
ed un task apposito per smistarli. Il metodo addObserver() standard
della classe Observable viene sostituito in modo da registrare i
vari oggetti sul Broker piuttosto che direttamente sull'istanza
di ThObservable. Quando quest'ultima chiama il proprio metodo notifyObservers(),
esso esegue solo il metodo update() del proprio Broker piuttosto
che quelli di tutti gli Observer registrati.
L'introduzione di un secondo metodo per la registrazione degli osservatori, addSyncObserver(), consente di dare una certa priorità a quegli oggetti che sono riconosciuti come sicuri in termini di correttezza e risorse impegnate. Ciò viene realizzato chiamando il metodo addObserver() originale. Nel nostro esempio, il modulo di registrazione su database, che è sempre eseguito ed è presumibilmente parte del sistema base, potrebbe essere ritenuto ben testato e sotto controllo e, quindi, essere registrato con addSyncObserver() invece che con addObserver().
package ThObservable;
import java.util.*;
public abstract class ThObservable extends Observable{
public class Notification {...}
private class Broker implements Observer, Runnable {...}private Broker b=new Broker();
private Thread bTh=null;
private long TMT=200;
private int priority=5;public ThObservable(int priority, long timeout){
super();
TMT=timeout;
this.priority=priority;
super.addObserver(b);
bTh = new Thread(b);
bTh.setDaemon(true);
bTh.setPriority(priority);
bTh.start();
}public synchronized void addObserver(Observer o){
super.deleteObserver(o);
b.addObserver(o);
}
public synchronized void addSyncObserver(Observer o){
b.deleteObserver(o);
super.addObserver(o);
}
public synchronized void deleteObserver(Observer o){
super.deleteObserver(o);
b.deleteObserver(o);
}
public synchronized int countObservers(){
return obs.size() + super.countObservers() - 1;
}public Notification getNotification(){return b.currentNotification;}
public void finalize(){
if(bTh!=null){
Thread t=bTh;
bTh=null;
t.notify();
}
}
}
Il codice di Broker, che svolge la maggior parte del lavoro, è in buona parte copiato da quello originale di Observable. Il loop infinito del thread rimane in sospeso su una wait() quando la coda è vuota e non impegna inutilmente la CPU. Il metodo update(), richiamato da ThObservable, si limita ad accodare l'oggetto e risvegliare il thread con notify(). Gli oggetti estratti dalla coda sono passati come parametro ai metodi update() di tutti gli osservatori registrati. Tali metodi sono eseguiti all'interno di un blocco try/catch e, nel caso venga intercettata un'eccezione, la registrazione dell'Observer colpevole viene imediatamente rimossa.
Il parametro di notifyObserver() è abbinato ad un oggetto di tipo Notification che può essere ottenuto dagli Observer chiamando il metodo getNotification() di ThObserver. Questo appesantimento si rende necessario a causa di una particolarità della coda usata: per evitare che un susseguirsi troppo rapido di eventi sovraccarichi il sistema, due riferimenti allo stesso oggetto non sono mai inseriti nella coda: farlo sarebbe inutile in quanto l'oggetto rimane sempre uno e, se modificato una seconda volta, i dati precedenti sono comunque persi. Per mantenere almeno traccia delle varie modifiche che il sistema non è riuscito a smistare, esse sono contate dentro un'istanza di Notification che, inoltre, mantiene gli estremi dell'intervallo di tempo tra il primo cambiamento e l'ultimo:private class Broker implements Observer, Runnable {
Hashtable pars=new Hashtable();
private class Queue{...}
private Queue q=new Queue(null);
private Vector obs;
private Observer[] arr = new Observer[2];
Notification currentNotification=null;Broker() {obs = new Vector();}
synchronized void addObserver(Observer o) {
if (!obs.contains(o)) { obs.addElement(o);}
}
synchronized void deleteObserver(Observer o){
bs.removeElement(o);
}
public void run(){
while(Thread.currentThread()==bTh){
synchronized(this){
currentNotification=q.get();
if(currentNotification==null){
try{ wait(); }catch(Exception e){}
continue;
}
}
int size=0;
synchronized (this) {
size = obs.size();
if(size>arr.length) {arr = new Observer[size];}
obs.copyInto(arr);
}
for(int i = size -1; i>=0; i--){
if(arr[i] != null){
try{
arr[i].update(ThObservable.this,
currentNotification.obj);
}catch(Exception e){
deleteObserver(arr[i]);
e.printStackTrace();
}
}
}
}
}
public synchronized void update(Observable obs,Object par){
q.put(par);
notify();
}
}
public class Notification {Infine, la classe annidata Queue, privata per Broker, mantiene tutte le istanze non ancora inoltrate di Notification. Un ThObservable può rilasciare notifiche prive di parametro (cioè con parametro null che non può essere usato come chiave nella Hashtable usata per implementare Queue); di conseguenza è fatta una gestione particolare del valore nullo che viene temporaneamente tradotto in un riferimento a this. Questo ha senso in quanto la classe Queue è privata e non genera conflitti con gli eventuali oggetti pubblici utilizzabili come parametri per gli Observer.
public long first=System.currentTimeMillis();
public long last;
public long num=1;
public Object obj;
Notification(Object obj){
this.obj=obj;
last=first;
}
}
private class Queue{Analisi di ThObservable
private Queue next=null;
private Queue tail=null;
Notification not;
Queue(Notification not){this.not=not;}
synchronized void put(Object par){
if(par==null) par=this;
Notification n=(Notification)pars.get(par);
if(n!=null){
n.last=System.currentTimeMillis();
n.num++;
return;
}
n=new Notification(par);
pars.put(par, n);
Queue q=new Queue(n);
if(next==null) next=q;
else tail.next=q;
tail=q;
}
synchronized Notification get(){
Queue q=next;
if(next!=null) {
next=next.next;
if(next==null) tail=null;
pars.remove(q.not.obj);
if(q.not.obj==this) q.not.obj=null;
return q.not;
}
return null;
}
synchronized boolean hasMoreElements(){return (q.next!=null);}
}
public class AcquisisciDati extends ThObservable implements Runnable{
La buona notizia
è che gli osservatori non hanno bisogno di alcuna modifica. Sostituendo
alla classe Observable della libreria standard la nuova versione
asincrona si otterranno performance migliori nella maggior parte dei casi
e, soprattutto, si manterrà un controllo più accurato sul
comportamento dei vari oggetti osservatori.
Allocazione
del tempo e blocco delle risorse
Resta in sospeso
il settimo e famigerato punto della specifica: non consentire "...che
metodi update() dall'esecuzione troppo lunga monopolizzino la CPU".
Mi vengono in mente diverse soluzioni, ma nessuna di applicazione così
generale da essere inserita nella classe astratta ThObservable:
public class Virus implements Observer{
public Virus() {super();}
public void update(Observable obs, Object param){
if(obs instanceof ThObservable.Notification) {
Object obj=((ThObservable.Notification)obs).obj;
new BadBoy(obj);
}
}
private class BadBoy implements Runnable{
private Object victim;
private Thread me;
badBoy(Object victim){
this.victim=victim;
me=new Thread(this);
me.start();
}
public void run(){
synchronized(victim){while(true);}
}
}
}
}
In merito è necessario precisare che un oggetto Java non è di per sè un monitor anche se tutti i suoi metodi sono dichiarati synchronized: la caratteristica principale che deve possedere un monitor è quella di essere l'unico responsabile per il proprio lock. Se chiunque in possesso di un riferimento ad un oggetto può bloccare a tempo indeterminato tutti i sui metodi sincronizzati allora il guscio offerto dall'incapsulazione del codice dentro una classe appare assai fragile!
Per le classi pubbliche è preferibile rinunziare ai metodi sincronizzati ed utilizzare, in loro vece, una sincronizzazione esplicita su un oggeto privato e, quindi, inaccessibile all'esterno. Ecco come implementare una classe che possa essere passata in modo sicuro come parametro agli Observer:
public class MailBox implements RtSource{
...
private Object mailBox=null;
private Object cond=new Object();
public Object read(){
synchronized(cond) {
if(mailBox==null)
try{cond.wait();}catch(Exception e) {}
return mailBox;
}
}
public void write(Object obj){
synchronized(cond) {
mailBox=obj;
cond.notify();
}
}
}
RtObservable:
una versione multithreaded di ThObservable
Segue il codice
di RtObservable che risolve il problema dell'accoppiamento residuo
tra i vari client collegati.
Quì viene
creato un thread con coda per ogni Observer.
Inoltre gli oggetti
di tipo Notification sono presi da un pool invece che essere continuamente
creati con new: in questo modo si cerca di diminuire il lavoro del
garbage collector ottimizzando le perfomance genarli del sistema. Questa
tecnica può funzionare solo se il riferimento al parametro di update()
non viene conservato dall'osservatore per un uso successivo. Se non si
ritiene che tale contratto possa essere rispettato, bisognerà
eliminare dal codice la variabile pool ed i metodi obtainNotification()
e returnNotification() rimpiazzandoli col consueto uso di new.
package RtObservable;Conclusioni
import java.util.*;
public abstract class RtObservable extends Observable{
private Hashtable brokers=new Hashtable();
private Notification pool=null;public class Notification {
public long first=System.currentTimeMillis();
public long last;
public long num=1;
public Object obj;
Notification(Object obj){
this.obj=obj;
last=first;
}
}
synchronized Notification obtainNotification(Object par){
Notification n;
if(pool!=null){
n = pool;
pool=(Notification)pool.obj;
n.first=System.currentTimeMillis();
n.last=n.first;
n.obj=par;
}else{
n=new Notification(par);
}
return n;
}
synchronized void returnNotification(Notification n){
n.obj=pool;
pool=n;
}
public class Broker implements Observer, Runnable {
Observer obs;
int priority=5;
Thread bTh=null;
private Hashtable pars=new Hashtable();
private Queue q=new Queue(null);
Notification currentNotification=null;private class Queue{
private Queue next=null;
private Queue tail=null;
Notification not;
Queue(Notification not){this.not=not;}
synchronized void put(Object par){
if(par==null) par=this;
Notification n=(Notification)pars.get(par);
if(n!=null){
n.last=System.currentTimeMillis();
n.num++;
return;
}
n=obtainNotification(par);
pars.put(par, n);
Queue q=new Queue(n);
if(next==null) next=q;
else tail.next=q;
tail=q;
}
synchronized Notification get(){
Queue q=next;
if(next!=null) {
next=next.next;
if(next==null) tail=null;
pars.remove(q.not.obj);
if(q.not.obj==this) q.not.obj=null;
return q.not;
}
return null;
}
synchronized boolean hasMoreElements(){return (q.next != null);}
}
Broker(Observer obs, int priority){
this.obs=obs;
this.priority=priority;
bTh=new Thread(this);
bTh.setDaemon(true);
bTh.setPriority(priority);
bTh.start();
}public void run(){
while(Thread.currentThread()==bTh){
synchronized(this){
currentNotification=q.get();
if(currentNotification==null){
try{ wait(); }catch(Exception e){}
continue;
}
}
try{
obs.update(RtObservable.this, currentNotification.obj);
}catch(Exception e){
deleteObserver(obs);
e.printStackTrace();
}
synchronized(this){
returnNotification(currentNotification);
currentNotification=null;
}
}
}
public synchronized void update(Observable obs, Object par){
q.put(par);
notify();
}
}
public RtObservable(){super();}
public synchronized void addObserver(Observer o, int priority){
if(brokers.get(o)==null){
Broker b=new Broker(o, priority);
brokers.put(o, b);
super.addObserver(b);
}
}
public synchronized void addObserver(Observer o){
addObserver(o, Thread.currentThread().getPriority());
}
public synchronized void addSyncObserver(Observer o){super.addObserver(o);}
public synchronized void deleteObserver(Observer o){
Broker b=(Broker)brokers.get(o);
if(b==null){
super.deleteObserver(o);
}else{
synchronized(b){
super.deleteObserver(b);
brokers.remove(b);
b.bTh=null;
b.notify();
}
}
}
public synchronized void deleteObservers(){
super.deleteObservers();
for(Enumeration e=brokers.elements();e.hasMoreElements();){
Broker b=(Broker)e.nextElement();
synchronized(b){
brokers.remove(b);
b.bTh=null;
b.notify();
}
}
}public Notification getNotification(Observer o){
return ((Broker)brokers.get(o)).currentNotification;
}
}
Bibliografia
Piergiuseppe Spinelli svolge attività di analista/programmatore dal 1980. Si è occupato di training e di sviluppo di sistemi, particolarmente nel campo della supervisione di processo. Ha lavorato per aziende dei gruppi Saint Gobaing, Angelini, Procter&Gamble, Alcatel/Telettra, SIV e per vari enti pubblici e privati. E contattabile all'indirizzo spinellip@sgol.it. |
|
||
|
||
MokaByte ricerca
nuovi collaboratori
|
||
|