Un argomento molto importante, quando si parla di servlet, è certamente quello che riguarda la gestione oculata del multithreading.
Generalmente, le classi definite in Java sono "multi-threaded", ovvero sono potenzialmente in grado di gestire l'invocazione dei propri metodi da parte di più thread in contemporanea, fanno eccezione tutti i metodi caratterizzati dalla parola riservata synchronized
che, come si è visto in un precedente articolo, rende tali metodi accessibili solo in maniera sequenziale (e, pertanto, non concorrente) da parte dei thread che ne richiedano l'invocazione.
È importante sottolineare che in applicazioni Java di tipo stand-alone il multithreading rimane, spesso, inutilizzato poiché la maggior parte delle classi implementate per tali applicazioni vengono poi eseguite da un singolo thread.
Si pensi per esempio ad applicazioni tipo "Hello World": il più delle volte in questi casi si scrive una classe con un solo metodo main()
che gestisce al suo interno la gran parte (se non tutto) del flusso applicativo necessario. Quando questa classe viene mandata in esecuzione, la Java Virtual Machine crea un singolo thread di esecuzione che si prende carico di eseguire il metodo main
.
Perché allora è importante il multithreading per le Servlet? La risposta è semplice. Le Servlet lavorano in un contesto diverso: il Web. Attraverso l'uso del protocollo HTTP, infatti, le richieste provenienti dai più svariati client connessi ad Internet saranno presumibilmente e frequentemente delle richieste concorrenti.
Questa affermazione è tanto più significativa quanto più alto è numero di accessi giornalieri al sito. In questo infatti caso la probabilità di richieste concorrenti è molto elevata. Ecco, dunque, che si spiega la necessità di sviluppare delle Servlet che siano sicure per il multithreading, ovvero siano thread safe.
Una Servlet non sicura per il multithreading (Thread unsafe)
Vediamo, allora, quali possano essere i problemi legati alla gestione errata del multithreading in una Servlet. Il modo migliore per farlo è quello di andare a vedere il codice di una servlet che NON sia thread safe.
Listato 1. Metodo doPost() della servlet
...
public class UnsafeServlet extends javax.servlet.http.HttpServlet
implements javax.servlet.Servlet
{
...
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String userID = request.getParameter("userID");
String password = request.getParameter("password");
...
try
{
String sleeptime = getInitParameter("sleep");
int sleep = Integer.parseInt(sleeptime);
Thread.sleep(sleep);
}
catch(Exception exc)
{
log("",exc);
}
try
{
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.println("<html><body>");
// stampa i valori ottenuti dal form
writer.println("<p><u>Valori delle variabili locali al metodo doPost()</u> <br />");
writer.println("userID=" + userID + "<br/>");
writer.println("password=" + password + "</p>");
// stampa i valori della servlet
writer.println("<p><u>Valori degli attributi della Servlet</u><br />");
writer.println("userID=" + this.userID +"<br />");
writer.println("password=" + this.password + "</p>");
writer.println("</body></html>");
writer.close();
}
catch (Exception exc)
{
exc.printStackTrace();
}
}
}
Prima di andare a capire attraverso il codice le cause che rendono thread unsafe la precedente Servlet, proviamo ad eseguirla. Avvaliamoci, a tal fine, di una semplice pagina HTML che ci consenta di inserire due campi: userID e password.
Listato 2. Form HTML per inserire i valori
...
<h1>Login</h1>
Inserire nei campi sottostanti la propria UserID e Password:
<form action="UnsafeServlet" method="POST">
<p><input type="text" name="userID" length="40" /></p>
<p><input type="password" name="password" length="40" /></p>
<p><input type="submit" value="Submit" /></p>
</form>
...
Salviamo la nostra pagina HTML come "logon.html" ed effettuiamo il deploy della servlet sul nostro application server (ad esempio, Tomcat), avendo cura di specificare nel deployment descriptor il seguente parametro di inizializzazione:
Listato 3. Codice da aggiungere al file di configurazione
<init-param>
<param-name>sleep</param-name>
<param-value>10000</param-value>
</init-param>
A questo punto apriamo il nostro browser e digitiamo l'indirizzo http://127.0.0.1:8080/UnsafeServlet/logon.html
.
Riempiamo i campi con UserID e Password a nostra scelta e premiamo il pulsante Invia. Dopo 10 secondi otterremo la pagina web generata dalla nostra servlet.
Fin qui nulla di straordinario e sembra anche che le cose funzionino in modo corretto. Ma, al di là del risultato ottenuto, l'errore è proprio dietro l'angolo. Per capirlo basta fare attenzione al fatto che finora abbiamo invocato una Servlet da un unico client, un'unica volta. Proviamo allora a simulare un accesso concorrente alla nostra servlet e vediamo adesso cosa accade: lanciamo due istanze del browser ed in entrambe carichiamo la URL utilizzata in precedenza.
Riempiamo i campi in entrambe le finestre avendo l'accortezza di utilizzare delle stringhe differenti per UserID e Password nei due browser. A questo punto, clicchiamo sul pulsante Invia della prima finestra ed attendiamo 3-4 secondi prima di cliccare sul pulsante Invia del secondo browser.
Cosa è accaduto? Contrariamente a quanto ci si aspettasse, i valori degli attributi della classe UnsafeServlet
visualizzati nella finestra del primo browser sono uguali a quelli mostrati nella finestra del secondo browser. Andando ad esaminare il codice della servlet, allora, ci accorgiamo che userID e password sono definite sia come proprietà della classe UnsafeServlet
, sia come variabili del metodo doPost()
della classe stessa.
Ogni volta che viene effettuata una richiesta alla servlet (attraverso il metodo doPost()
), sia le variabili del metodo, sia le proprietà della classe vengono valorizzate con i valori provenienti dalla request.
Il problema di fondo, però, è che le proprietà della classe sono condivise tra tutti i thread che accedono alla servlet e pertanto suscettibili di modifiche da parte di ogni thread successivo al primo.
È bastato, quindi, ritardare di qualche secondo la risposta della servlet (avvalendoci del parametro di input sleep
definito nel file "web.xml") per far sì che giungesse il secondo thread a modificare i valori degli attributi di classe.
Quando il primo thread restituisce la risposta al client, in sostanza, i valori delle proprietà di classe sono stati già cambiati dal thread scatenato dalla richiesta del secondo browser.
Questo, naturalmente, è un semplicissimo esempio che non fa nulla di particolare e, soprattutto, non crea problemi pratici a nessuno. Si pensi a cosa accadrebbe se un comportamento del genere si presentasse quando ad essere in gioco fossero delle informazioni di estrema importanza. Le conseguenze non sarebbero sicuramente così banali!
Come rendere una Servlet thread-safe
È arrivato il momento di capire come fare ad evitare problemi simili a quelli appena visti e rendere, quindi, "thread safe" una servlet. Esaminiamo alcune norme di carattere generale che possono venirci in aiuto.
Usare variabili locali
Vanno utilizzate, per lo più, variabili locali (definite all'interno dei metodi) laddove si renda necessario salvare il valore di uno o più dati provenienti da una determinata richiesta. In tal modo, ogni thread che invochi i metodi della Servlet (come ad esempio la doPost()
dell'esempio precedente) utilizzerà la propria copia di variabili locali e in nessun modo potrà verificarsi uno scambio di valori con altri thread concorrenti.
Come usare le proprietà delle Servlet
Utilizzare le proprietà di classe di una Servlet (member variables) soltanto quando si è certi che i dati in esse contenuti non subiscano variazioni. Solitamente fanno parte di questo contesto tutte quelle variabili membro che vengono inizializzate all'avvio della Servlet e rimangono invariate per l'intero ciclo di vita della Servlet stessa. Ad esempio, una stringa di connessione ad un database o il path che identifichi una determinata risorsa di sistema potrebbero essere delle informazioni che ben si adattano a tale scelta.
Nei casi in cui si sia scelto di utilizzare una o più proprietà della Servlet per salvare e/o modificare determinate informazioni provenienti dalle richieste dei client, sarà opportuno proteggere tali variabili in modo da renderne, comunque, l'accesso sincronizzato ed evitarne l'accesso concorrente.
Se la Servlet ha accesso a risorse esterne (come, ad esempio, un file) sarà bene proteggere l'accesso a tali risorse in modo da renderlo sincronizzato. Non è raro imbattersi in errori causati dalla sovrapposizione di operazioni di lettura/scrittura (ad esempio, leggere il contenuto di un file mentre un altro thread ne sta modificando il contenuto).
Stratagemmi da evitare
È da scartare, invece, la pratica piuttosto comune che prevede l'implementazione dell'interfaccia SingleThreadModel
, ovvero quella di definire la propria Servlet nel seguente modo:
public class MyServlet implements SingleThreadModel
La SingleThreadModel
è una interfaccia priva di metodi (le interfacce che non definiscono alcun metodo sono anche dette "marker interfaces") che fa sì che il container della Servlet garantisca ad un singolo thread per volta l'accesso alla classe della Servlet stessa, in modo da evitare a priori possibili accessi concorrenti. Naturalmente, per poter gestire in maniera efficiente le richieste provenienti dai vari client (non sarebbe pensabile fare attendere un visitatore di un sito web che una servlet soddisfi prima tutte le precedenti richieste pervenute!), in questo caso il container provvederà a creare un pool di istanze della Servlet..
Tuttavia, come preannunciato, tale soluzione non riesce comunque a garantire che una servlet sia thread safe. Infatti, il problema dell'accesso ad eventuali risorse esterne rimane irrisolto e, inoltre, qualora venissero utilizzate variabili di classe statiche, è evidente che il loro valore sarebbe condiviso da tutte le istanze della servlet create dal container. Infine, ma non meno trascurabile, c'è da considerare che una soluzione del genere avrebbe dei grossi limiti a livello di scalabilità: al crescere delle richieste sarebbe necessario incrementare il pool di servlet ed è chiaro che tale incremento diverrebbe presto ingestibile.
Da non adottare sicuramente, infine, è l'idea di inserire la parola chiave synchronized
per i metodi doPost
e doGet
. Il risultato sarebbe quello di mantenere in costante attesa i visitatori del vostro sito... a meno che non preventiviate di ricevere pochissime visite!
Conclusioni
La gestione accorta del multithreading è una delle cose che non va mai trascurata quando si sviluppa una Servlet. Soltanto tenendo presenti alcuni semplici accorgimenti si eviterà di incorrere in errori anche gravi. Ricordiamoci sempre che i thread possono dare un grande valore aggiunto ma vanno sempre usati con la massima attenzione.
Alla prossima.