Login
Cerca all'interno di JavaPortal
Help
Home Page Documentazione Forum Progetti Partner Pubblica!
Documentazione > Tutorial > Implementazione di una sessione thread-scoped : il pattern Registry.
Hide
Best Practices
EJB
Frameworks
Howto
J2EE
J2ME and Wireless
J2SE
JSP e Servlet
Java Application Server
Java IDE/Tools
Java Media
Java Security
Java Sys Admin
Java e XML
Java e SQL
OpenSource Java
Patterns
Repository
Tesi
UML
Web Services
Slide
White Paper di jws.it
project management
Eventi
Groovy

Hai una tesi in Java?
Tesine preparate
per esami?
Pubblica tutto su
JavaPortal!

Scrivi al nostro staff


Project Kenai


Un giorno le macchine riusciranno...
a risolvere tutti i problemi, ma mai nessuna di esse potra' porne uno. Einstein


Come usare una classe java per interagire con xml (seconda parte)


Rss Feed
Home Page
Articoli
News
Forum
Classi

  Visualizza Commenti (1) Aggiungi Commento    
Add to Shortcuts
 
Vota l'articolo
Implementazione di una sessione thread-scoped : il pattern Registry.
By Andrea Gazzarini
7 giugno 2005

  Implementazione di una sessione thread-scoped : il pattern Registry.
Program Cenni sui principali tecniche di object reusing
Program Il design pattern Registry: brevi cenni
Program Breve cenno alla classe ThreadLocal
Program Implementazione e costruzione di una thread session
Program Bibliografia

L'ottimizzazione e l'utilizzo razionale delle risorse sono due tra gli obiettivi fondamentali in ambito di applicazioni enterprise. Il riutilizzo di oggetti attraverso meccanismi di pooling (es. Connection pooling), canonicalizzazione (es. interfacce enumerative di costanti), caching (es: singleton, flyweight, service locator) risulta già ampiamente documentato in letteratura.

Quella che analizzeremo in questo articolo è una particolare tecnica di caching la cui caratteristica principale risiede nel fatto di essere direttamente collegata al ciclo di vita di uno specifico thread, una sorta di sessione ad uso dello stesso in cui i partecipanti ad una specifica sequenza di flusso possono condividere degli oggetti.



Cenni sui principali tecniche di object reusing top

Anche se esiste fior di documentazione sulle varie tipologie e tecniche di object reusing, citiamo brevemente le più diffuse onde apprezzare tra l'altro le relative differenze.

1.1    Object Pooling.

Tale tecnica viene utilizzata solitamente laddove sussistono le seguenti condizioni:

  • Utilizzo intensivo di una specifica tipologia di oggetti (es: java.sql.Connection, java.lang.Thread, java.awt.Image);
  • Costo marginale di creazione (instanziazione) dello specifico oggetto medio/alto.
  • Stato dell'oggetto non rilevante per l'utilizzatore: l'interesse è diretto al comportamento più che allo stato.

L'utilizzo di questa tecnica presuppone l'esistenza oltre che del pool, anche di un suo gestore, che fornisce la logica di prenotazione/rilascio degli oggetti in cache nei confronti degli utilizzatori.

Quando viene richiesto l'utilizzo di un oggetto al pool, il suo gestore fornisce il primo disponibile; qualora non vi siano oggetti disponibili ne viene creato uno nuovo. In questo senso tale tecnica deriva gran parte della sua teoria dal design pattern "FlyWeight" [Gang of Four].

Valga per tutti l'esempio del connection pooling utilizzato per l'ottimizzazione dell'ottenimento delle connessioni verso una sorgente dati.

Analogo esempio è costituito dai pool di thread, anch'essi oggetti molto onerosi da un punto di vista creazionale, tanto da meritare un pattern dedicato in "Core J2EE Pattern" (Worker Thread).

1.2    Canonicalizzazione.

    E' una tecnologia di reusing utilizzata anche all'interno della stessa JVM (per il caching delle stringhe). Differisce dall'object pooling principalmene per l'assenza (almeno esplicita) di un gestore che si occupa del management del pool (rilascio, prenotazione, ampiamento, riduzione, etc...). Altra differenza riguarda il fatto che gli oggetti utilizzati nella canonicalizzazione sono immutabili e quindi ad uso contemporaneo di più client. In tal senso rispetto all'object pooling tali oggetti sono rilevanti per il loro stato (uguale per tutti) e non per il loro comportamento; proprio per questo tale tecnica si presta in quelle situazioni ove gli oggetti da condividere sono immutabili (Stringhe, wrapper dei tipi primitivi, etc...).

La canonicalizzazione consiste nell'individuare degli oggetti che sono (per il valore del loro stato) suscettibilii di riutilizzo da parte di altri oggetti, e renderli disponibli al riutilizzo ogni qualvolta vengono richiesti (piuttosto che crearne di nuovi con lo stesso stato). Vediamo rapidamente di seguito due esempi.

1.2.1    Canonicalizzazione delle stringhe (Java Virtual Machine).

Ogni qualvolta nel nostro codice creiamo una stringa con un valore letterale (analizzabile a livello di compilazione), la JVM memorizza tale stringa in una sua apposita cache interna, onde riutilizzarla qualora venga richiesta di nuovo. Per il
programmatore la cosa è trasparente:

String unaStringa = "nota bene: valore letterale";


Ora, se la reference unaStringa è un nuovo oggetto o punta ad un oggetto già esistente (precedentemente creato e memorizzato dalla JVM) questo è assolutamente indifferente per chi la usa (si ricorda che le stringhe sono oggetti immutabili).

Il vantaggio è ovvio: per quante righe di codice ci siano che richiamano il metodo ove si trova la riga riportata sopra esisterà in memoria un solo oggetto con il contenuto "nota bene: valore letterale". Ho precisato valore letterale perché la gestione descritta non si applica alle stringhe il cui valore non può essere determinato se non a runtime. Per esempio:

public String greet(String name) {

                   return "Hello"+name;// Non viene canonicalizzato

}

Al di là del cattivo utilizzo della concatenazione (meglio uno StringBuffer), quello che conta ai fini del discorso è che la stringa ritornata dal metodo non viene canonicalizzata. Per forzare la canonicalizzazione occorre utilizzare il metodo intern() sulla stringa stessa.

1.2.3    Utilizzo della canonicalizzazione attraverso interfacce enumerative.

Questo utilizzo che viene fatto della canonicalizzazione (lato programmazione) è un pò diverso dal precedente e si riduce sostanzialmente alla creazione di interfacce enumerative di costanti utilizzate per puntare a valori di default immutabili.

public interface Costants {

      Integer ZERO= new Integer(0);

      Date NEVER = new Date(Long.MAX_VALUE);

}           

Ogni qualvolta nel nostro codice una qualsiasi classe possiede un attributo di tipo Integer che deve essere valorizzato a zero, piuttosto che creare un nuovo oggetto con new Integer(0) lo si fa puntare alla costante definita nell'interfaccia:

mioInteger = Constants.ZERO;

ottenendo così un risparmio in termini di oggetti creati. Trattandosi di costati final e static definite in una interfaccia, la loro disponibilità è immediata all'interno della JVM e non hanno quindi bisogno di essere inserite all'interno di singleton o altro tipo di gestore (altra differenza rispetto al pool di oggetti).



Il design pattern Registry: brevi cenni top

"A well known object that other objects can use to find common objects and services".

Questa è la definizione del pattern Registry riportata in "Patterns of Enterprise Application Architecture" di Martin Flower. Sostanzialmente stiamo parlando, come dice la parola stessa, di un registro ove memorizzare degli oggetti la cui necessità di utilizzo può presentarsi più volte nel corso dello svolgimento del flusso dell'applicazione. Ora, se sopratutto la creazione di questi oggetti è una operazione onerosa, allora l'idea di fondo è quella di memorizzare per poi recuperare e riutilizzare in un secondo momento. Non solo: avere a disposizione un'area comune permette a oggetti non direttamente legati tra di loro di comunicare scambiandosi oggetti.

Per quanto riguarda l'implementazione concreta del registry, l'autore specifica che essa varia a seconda dello scope in cui il registry stesso dovrà essere disponibile, proponendo soluzioni che prevedono variabili statiche (application scope), incapsulamento del registry in un singleton (process-scope) ed infine un caso particolare che è proprio quello di cui parleremo in questo articolo (thread-scope).



Breve cenno alla classe ThreadLocal top

"This class provides ThreadLocal variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal objects are typically private static variables in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)." (JavaDoc 1.3.1)

La classe ThreadLocal permette di associare ad ogni thread un proprio oggetto con un proprio stato. Attraverso infatti i metodi get e set il thread corrente può impostare e successivamente ritirare degli oggetti che saranno indissolubilmente legati a lui.

Per capirci, la classe ThreadLocal è una sorta di Map in cui la chiave è Thread.currentThread (il thread corrente) e il valore è l'argomento del metodo set(Object). Instaurandosi questa relazione chiave-valore (thread corrente-oggetto memorizzato), il recupero di quell'oggetto potrà avvenire solamente ad opera dello stesso thread che lo ha inserito.

Nella implementazione che presento di seguito viene utilizzata questa classe per associare il registry (oggetto valore) ad ogni thread (oggetto chiave).



Implementazione e costruzione di una thread session top

In questo capitolo si mette in pratica quanto detto sinora al fine di costruire una implementazione pratica del pattern Registry per la messa in uso di una sessione thread-scoped.

Per spiegare che cosa intendo per sessione, basti pensare alla javax.servlet.HttpSession; consiste in una sorta di cache dove poter memorizzare dei valori associati a dei nomi e successivamente, attraverso tali nomi, recuperare i valori. Tale sessione viene utilizzata da componenti (JSP, Servlet) che non sono a conoscenza l'uno dell'altro, ma eventualmente comunicano inserendo e recuperando degli oggetti (nella sessione).

In ambito di sviluppo di applicazioni distribuite, il concetto di thread è stato lasciato (ingiustamente) in disparte, forse alle volte per una erronea interpretazione delle specifiche J2EE che vietano l'utilizzo dei Thread all'interno degli EJB. Ebbene, bisogna ricordare che, seppur osservando tali specifiche alla lettera, il concetto di thread esiste a prescindere dal fatto che noi vogliamo utilizzarli o meno. In java tutto ciò che è dinamico è un thread; un metodo viene eseguito dai thread; il flusso di sequenza di una applicazione viene eseguito dai thread, ciò che in un sequence diagram permette di spostarsi da un partecipante ad un altro (le frecce) chiamando metodi, e spedendo messaggi è un thread.

Analizziamo il sequence diagram presente in figura 1. Al di là del flusso che descrive, notiamo l'esistenza di una serie di partecipanti coordinati e creati da una sequenza di operazioni al fine di realizzare un determinato scopo. Anche se non vengono specificati i ritorni (le frecce da destra verso sinistra) con una penna si può seguire benissimo il flusso che parte dall'attore principale (l'omino in alto a sinistra), crea il primo oggetto e via via sino ad arrivare all'ultimo partecipante che ritornando un certo risultato all'utilizzatore, chiude il giro.

Fig. 1 Implementazione di una sessione thread-scoped : il pattern Registry.

Partendo dal presupposto che

  • non vengono creati esplicitamente nuovi thread da codice all'interno del flusso;

  • i componenti che intervengono nel diagramma di sequenza si trovano tutti all'interno della stessa JVM; cioè non esistono chiamate remote tra i partecipanti.


ci domandiamo:  chi è che esegue tutti gli step descritti nel diagramma con la giusta sequenza, interrogando i componenti nell'ordine descritto? Si tratta di un thread, il quale, fermo restando i presupposti di cui sopra, passa attraverso tutti gli oggetti invocandone i metodi e completando il flusso descritto.

All'interno dei metodi possiamo ottenere una reference a questo thread tramite il metodo Thread.currentThread().

L'idea di fondo di questo esempio si basa proprio sul concetto appena visto: l'esistenza di un comune oggetto (il thread esecutore) tra tutti gli step di un determinato processo.

Supponiamo che l'ultimo partecipante del diagramma (quello più a destra) abbia bisogno di una particolare oggetto che viene creato invece in un metodo faiQualcosa() del primo partecipante (quello più a sinistra dopo l'omino). La prima soluzione che viene in mente è quello di passare come parametro tale oggetto a chi se ne deve servire. Esiste in tal senso un piccolo problema: il produttore dell'oggetto ed il suo consumatore non sono legati direttamente, ma tramite una miriade di altri oggetti che intervengono nel flusso su step intermedi. Secondo la soluzione proposta dovremo passare come parametro l'oggetto in questione tra tutti i componenti intermedi tra i due partecipanti, obbligando quindi a sporcare le interfacce (che presentano nella firma dei metodi il parametro in questione che in realtà non serve a nulla, solamente come passamano per poter arrivare all'utilizzatore finale).

La soluzione più pulita risulta essere una sorta di cache in cui memorizzare e successivamente recuperare l'oggetto che è necessario condividere. La prima cosa che viene in mente in tal senso è un singleton. Ma se lo stato di tale oggetto cambia ogni qualvolta che il flusso descritto nel sequence ha inizio, allora forse una persistenza (in memoria) come quella offerta dal singleton è superflua ed obbliga ad implementare meccanismi di refresh (i singleton si prestano molto bene per mantenere delle cache di dati immutabili di sola lettura).

Quello che serve è una cache "temporanea" che ha un ciclo di vita legato alla durata del thread esecutore (thread-scoped). Qui i produttori memorizzano gli oggetti da riutilizzare associandogli un nome e i consumatori li recuperano (tramite quel nome) in seguito utilizzandoli. Tale soluzione è completamente indipendente dal modo in cui sono relazionati gli oggetti in questione: basta semplicemente che facciano parte del flusso di sequenza (si trovino sulla strada del thread esecutore...).

5.1    I componenti.

Le classi coinvolte nella implementazione di una sessione ad uso thread sono molto semplici e sono due. Vediamo brevemente come sono fatte e come si relazionano tra di loro nel class diagram in figura 2.

Fig. 2 Implementazione di una sessione thread-scoped : il pattern Registry. 

Come si capisce dal nome, la classe ThreadSession è l'implementazione della sessione (il Registry dove si prelevano e recuperano gli oggetti memorizzati). Contiene quindi i metodi per memorizzate (setAttribute), recuperare (getAttribute) gli oggetti, pulire la cache (clear), etc... Gestisce una Map interna in cui sono presenti gli attributi memorizzati con i loro valori.

La classe ThreadSessionManager funge da coordinatore e gestore delle sessioni attive, fornendo un unico punto di ingresso (trasparente per l'utilizzatore) per ottenere/creare la sessione specifica dell'utilizzatore. Si tratta in sostanza di un singleton che mantiene una variabile privata di tipo java.lang.ThreadLocal in cui memorizza gli oggetti di tipo ThreadSession creati durante l'esecuzione dell'applicazione. Quando viene richiesta una sessione, si analizza, tramite il metodo Thread.currentThread(), il richiedente, viene verificato (tramite la classe ThreadLocal) se esiste una sessione associata ad esso e in caso positivo viene ritornata, mentre in caso negativo, viene creata, memorizzata e ritornata. Una successiva richiesta da parte dello stesso thread provocherà il recupero della sessione memorizzata (e creata) precedentemente.

5.2    L'esempio.

Ai fini di una migliore comprensione del funzionamento nonché dell'utilizzo dei concetti presentati sinora, prendiamo in considerazione un ipotetico scenario in cui esistono tre classi Prima, Seconda e Terza legate a catena tra di loro; cioè la classe Prima crea la classe Seconda la quale crea la classe Terza.

Fig. 3 Implementazione di una sessione thread-scoped : il pattern Registry

public class Prima {     

      public int esegui(String codice) {

                  // X può essere ottenuto solamente in questo punto

                  X x = businessDelegate.recuperaOggettoX(codice);

 

                  Seconda seconda = new Seconda();

                  double risultato = seconda.eseguiCalcolo(x.getQuantita);

                  return arrotonda(risultato);       

}

public int arrotonda(double valore) {

            ....

}

}

public class Seconda {

      public double eseguiCalcolo(int quantita){

                  int moltiplicatore =  ... vengono eseguiti una serie di calcoli

                  Terza terza = new Terza();

                  return terza.eseguiCalcoloPercentuale(moltiplicatore);                

}

}

public class Terza {

      public double eseguiCalcoloPercentuale(int moltiplicatore) {

                  // per eseguire questo calcolo abbiamo bisogno dell'oggetto X

                  // Creato nella classe Prima

                  return ....

}

}

Sia la classe Prima che la classe Terza (la prima e l'ultima della catena) hanno bisogno, per compiere il proprio lavoro, di un oggetto X che contiene un informazione quantitativa da utilizzarre. Le informazioni necessarie a recuperare tale oggetto sono a disposizione solamente della classe Prima attraverso l'istanza del  business delegate ed il parametro passato in ingresso al suo metodo esegui(String codice).
Il problema è per l'appunto come far arrivare tale oggetto X all'istanza di Terza di modo che anch'essa possa compiere il suo lavoro. Un primo approccio è quello di fare sì che la reference di X sia presente nella firma di ogni metodo intermedio e finale, cambiando così le classi in questo modo:

nella classe Seconda :     

    public double eseguiCalcolo(int quantita, X oggetto){

                  int moltiplicatore =  ... vengono eseguiti una serie di calcoli

                  Terza terza = new Terza();

                  return terza.eseguiCalcoloPercentuale(moltiplicatore,oggetto);                

}

mentre nella classe Terza:

      public double eseguiCalcoloPercentuale(int moltiplicatore, X oggetto) {

                  // per eseguire questo calcolo abbiamo bisogno dell'oggetto X

                  // Creato nella classe Prima

                  return oggetto.getQuantita * UNA_COSTANTE / 100.87;

}

In questo modo Terza ha a disposizione l'oggetto creato nella classe Prima e tutto funziona. Certo che, da un punto di vista di pulizia del codice il tutto lascia un pò a desiderare, visto che siamo obbligati a far passare tale reference tra tutti gli step intermedi (nell'esempio ce ne è uno solo, ma provate ad immaginare la situazione come si presenterebbe nel sequence diagram riportato in figura 1...!) ed inoltre a sporcare le interfacce con parametri che, per quanto riguarda gli step intermedi, hanno solamente una funzione di transito: ad un primo sguardo non è molto chiaro a cosa serva il parametro oggetto nel metodo della classe Seconda.

La soluzione più pulita prevede l'utilizzo di una sessione ad uso del thread esecutore del flusso che abbiamo analizzato. Partiamo da come cambia il codice delle tre classi.

    public class Prima {    

      public int esegui(String codice) {

                  X x = businessDelegate.recuperaOggettoX(codice);

                  // Ottenimento della sessione per questo thread corrente

                  // e memorizzazione della reference x con chiave stringa "OGGETTO_X". Tale

                  // stringa verrà poi utilizzata (classe Terza) per recuperare x.

                  ThreadSessionManager.getInstance().getSession().setAttribute("OGGETTO_X",x);

                  // Oppure in forma meno concisa...

                  ThreadSession session = ThreadSessionManager().getInstance().getSession();

                  session.setAttribute("OGGETTO_X",x);

                  Seconda seconda = new Seconda();

                  double risultato = seconda.eseguiCalcolo(x.getQuantita);

                  return arrotonda(risultato);       

}

public int arrotonda(double valore) {

            ....

}

}

// La classe Seconda non presenta più alcuna traccia di X

public class Seconda {

      public double eseguiCalcolo(int quantita){

                  int moltiplicatore =  ... vengono eseguiti una serie di calcoli

                  Terza terza = new Terza();

                  return terza.eseguiCalcoloPercentuale(moltiplicatore);                

}

}

public class Terza {

      public double eseguiCalcoloPercentuale(int moltiplicatore) {     

            // Recupero della sessione associata a questo thread e dell'attributo "OGGETTO_X"

                  // precedentemente creato e memorizzato nella sessione.          

                  X x = (X) ThreadSessionManager.getInstance().getSession().getAttribute("OGGETTO_X");

                            

                  // Oppure in forma meno concisa...

                  ThreadSession session = ThreadSessionManager.getInstance().getSession();

                  X x = (X)session.getAttribute("OGGETTO_X");

                  return x.getQuantita * UNA_COSTANTE / 100.87;

}

}

 
Viene di seguito riportato il sequence diagram che mostra l'utilizzo della ThreadSession.

Fig. 4  Implementazione di una sessione thread-scoped : il pattern Registry.

Notare come la classe Prima nello step 2 e 3, interfacciandosi con il ThreadSessionManager ottiene la sessione associata (associata al thread corrente). Tale operazione in realtà provoca (step 4) la creazione di una nuova istanza di un oggetto di tipo ThreadSession (è la prima volta che il thread richiede una sessione, non esiste e quindi viene creata). Successivamente (step 5,6) il componente Prima crea l'oggetto X e lo inserisce nella sessione (metodo setAttribute).

Continua poi il flusso di esecuzione sino ad arrivare alla classe terza, dove per effettuare il calcolo si necessita ancora di questo oggetto X. A questo punto il gioco è semplice: attraverso (step 11 e 12) il ThreadSessionManager si richiede la sessione associata al thread corrente (che è sempre lo stesso di prima!). Tale richiesta non provoca più la creazione di un nuovo oggetto, bensì la localizzazione della sessione associata al thread richiedente (step 13). Una volta ottenuta la sessione, l'oggetto di tipo Terza può richiedere l'attributo X precedentemente memorizzato (step 14 - metodo getAttribute) ed effettuare così i propri calcoli.

Ripeto,l'esempio presentato è alquanto semplice ed astratto, ma basti pensare ad un sequence come quello presentato all'inizio per immaginare come tale problema assuma delle dimensioni rilevanti. Le applicazioni pratiche di quanto presentato sinora sono a mio avviso numerose, poiché quando si parla di riutilizzo di oggetti, ottimizzazione, razionalizzazione delle risorse, non si finisce mai di effettuare aggiustamenti al proprio codice. Tenendo conto anche del fatto che la tecnica presentata costituisce un modo elegante per mettere in comunicazione due oggetti non direttamente legati tra di loro. Molti design pattern presentano una situazione di questo tipo; vediamo alcuni esempi.

Il pattern Chain of Responsibility costituisce una catena di oggetti in cui ognuno conosce l'anello successivo verso cui delegare eventualmente. Notare come in Fig. 5, aClient non conosce il secondo aConcreteHandler, né il successor contenuto in esso.


Fig. 5 Implementazione di una sessione thread-scoped : il pattern Registry

Il design pattern Decorator definisce una struttura annidata in cui si presenta una situazione molto simile a questa vista appena per il Chain of Responsibility. Ogni Decorato è annidato dentro il suo Decoratore in una struttura con una profondità indefinita a priori.

Fig. 6 Implementazione di una sessione thread-scoped : il pattern Registry

Il pattern Composite definisce una struttura ad albero di oggetti in cui ogni componente può essere nodo o foglia. Nella figura aComposite non è a conoscenza delle aLeaf appartenenti al aComposite figlio.

Fig. 7 Implementazione di una sessione thread-scoped : il pattern Registry



Bibliografia top

Gamma,Helm,Johnson,Vlissides - "Design Patterns: Elements of Reusable Object-Oriented Software", Addison-Wesley Pub Co, 1995;

Martin Flower, "Pattern of Enterprise Architecture";, Addison Wesley, Addison-Wesley Pub Co, 2002;

Deepak Alur, John Crupi, Dan Malks, "Core J2EE patterns",Prentice Hall PTR, 2001;

Jack Shirazi, "Java Performance Tuning", O'REILLY, & Associates, 2001;

Bill Venners, "Inside the Java 2 Virtual Machine", McGraw-Hill, 2003;

Mark Weiss, "Data Structures & Algorithm Analysis in Java", PeachPit Press, 2003;

Hay, "Data Model Patterns";, Dorset House, 1995.s



Username:
Password:
To sign up for an account, click register... Register
Hide





Powered By



Campagna Anti-IF


Skin


PARTNER
Zio Budda
HostingJava


LICENZA



Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto licenza Creative Commons

Sitemap  © 2002-2004 Copyright Information. Privacy . Today is domenica 1 agosto 2010