Java Tutorial - Parte 2 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Le code bloccanti

Introduzione

In questo capitolo il lettore imparerà i principi fondamentali delle blocking queues (=le code bloccanti) attraverso una app di test delle stesse. Le code sono strutture dati simili alle array nel senso che contengono un certo numero di elementi dello stesso tipo ma nelle quali, a differenza delle array, l'acceso agli elementi non è indirizzato da un indice; in una coda esiste il concetto di:

  • testa che rappresenta l'elemento inserito per primo
  • coda che rappresenta l'elemento inserito per ultimo

Una coda può essere FIFO (First In First Out = il primo elemento ad entrare è il prino ad usicre) oppure LIFO (=Last In First Out = l'ultimo elemento ad entrare è il primo ad uscire):

Le code bloccanti

La coda bloccante è definita dalla interfaccia BlockingQueue e la sua implementazione più semplice è la ArrayBlockingQueue.

Una coda bloccante supporta operazioni che attendono che la coda non diventi vuota quando si recupera un elemento e che lo spazio diventi disponibile nella coda quando si memorizza un elemento. I metodi di BlockingQueue sono disponibili in quattro forme, con diversi modi di gestire le operazioni che non possono essere soddisfatte immediatamente, ma possono esserlo in un momento futuro: una forma genera un'eccezione, la seconda restituisce un valore speciale (null o false, a seconda dell'operazione), la terza blocca il thread corrente indefinitamente finché l'operazione non riesce e la quarta blocca solo per un dato limite di tempo massimo prima di rinunciare. Questi metodi sono riassunti nella seguente tabella:

operazione solleva eccezione valore speciale bloccaggio timeout
inserimento add offer put offer
rimozione remove poll take poll
esamina element peek N.A. N.A.

Il contenuto del file compresso

Nella cartella javatutor2/projects/queues ho implementato una piccola app GUI che vi mostra il funzionamento di una coda bloccante. Nella seguente tabella sono riportati i files sorgente della app ed una loro breve descrizione:

nome file package descrizione
Main.java queues app principale
Feeder.java queues il feeder della coda bloccante
Consumer.java queues il consumer della coda bloccante
Speedometer.java queues.custcomp un tachimetro analogico
Speedograph.java queues.custcomp un tachigrafo a due dimensioni
Speedgauge.java queues.custcomp un tachimetro ed un tachigrafo
TestMeter.java queues.custcomp test del tachimetro
TestGraph.java queues.custcomp test del tachigrafo
TestMeter.java queues.custcomp test di entrambi i componenti

In questa app di test si crea una coda bloccante di massimo 50 elementi e due threads separati:

  • un Feeder che alimenta la coda: ad intervalli regolari questo thread genera una stringa di testo "message #X" e la inseirsce in coda; il messaggio accodato viene visualizzato in uno scroll-pane,
  • un Consumer che consuma la coda: ad intervalli regolari questo thread preleva un elemento dalla coda e lo visualizza un uno scroll-pane
  • ad intervalli regolari di un secondo, la app principale (il Main) ottiene la dimensione della coda e ne visualizza l'andamento in un componente personalizzato chiamato Speedgauge che ricorda vagamente il tachimetro ed il tachigrafo dei mezzi di trasporto.

Poichè gli intervalli di Feeder e del Consumer sono modificabili, appare chiaro che, nella modalità bloccante se il ritardo del feeder è maggiore di quello del consumer, la coda sarà quasi sempre vuota dal momento che il consumer è più veloce del feeder.
Di contro, se il ritardo del feeder è minore del consumer, allora è il feeder ad essere più veloce ed il consumer non riesce a stargli dietro: la coda tenderà a riempirsi fino alla capienza massima; raggiunta la capienza massima, anche il feeder rallenterà dal momento che il suo metodo put si blocca fino a che non si libera spazio nella coda.

Questo è uno screenshot della app di test:

Per eseguire questa piccola app spostatevi nella cartella javatutor2/projects ed eseguite il seguente comando:

>java -ea queues.Main

NOTA: il tachimetro ed il tachigrafo sono componenti GUI personalizzati che non esistono nella libreria standard di Java. Come si creano i componenti personalizzati viene descritto in Componenti personalizzati.

Il main

Non c'è molto da dire sulla classe Main; essa si occupa per lo più del disegno della inferfaccia grafica e delle azioni da compiere quando l'user clikka i due bottoni di comando:

  • start: questo metodo fa partire tutte e tre i threads
  • stop: questo metodo ferma tutti e tre i threads

Il Feeder

La classe Feeder, derivata da SwingWorker esegue il suo compito in background:

  • attende il ritardo specificato col metodo setDelay e, allo scadere, genera una stringa di testo di questo tipo: "message #X" dove "X" è un numero progressivo mantenuto in un membro statico che parte da UNO
  • ottenuto il messaggio lo accoda con il metodo BlockingQueue.put che, come visto poc'anzi, è un metodo che blocca il thread se non vi è più spazio nella coda
  • quando il metodo put rientra pubblica la stringa come risultato intermedio del thread

Voglio attirare la attenzione del lettore sul fatto che se la coda è piena significa che il consumer non riesce a stare al passo col feeder e quindi è corretto che esso rallenti; è nel metodo put che si ottiene questo rallentamento dal momento che quel metodo è bloccante.
Ma poichè il blocco avviene in un thread separato, il EDT continua invece a lavorare e la GUI rimane responsiva.

A proposito dei risultati intermedi nei threads, vi ricordo che sia il feeder che il consumer sono threads cosidetti infiniti anche se non sono mai veramente infiniti.
Se non ricordate come abbiamo gestito i threads infiniti potete rinfrescarvi la memoria leggendo la sezione Il thread SendQueue.

Il consumer

Anche la classe Consumer deriva da SwingWorker ed esegue il suo compito in background:

  • attende il ritardo specificato col metodo setDelay e, allo scadere, preleva una stringa dalla coda usando il metodo BlockingQueue.take il quale blocca il thread se la coda è vuota
  • se un elemento è stato ottenuto dalla coda esso viene pubblicato in modo che il EDT possa inserirlo nello scroll-pane del consumer

Anche in questo caso voglio attirare la attenzione del lettore sul fatto che se la coda è vuota significa che il feeder non riesce a stare al passo col consumer e quindi quest'ultimo deve rallentare. E' proprio il metodo take che provoca il rallentamento del consumer nel caso di coda vuota ma è ciò che ci serve e, comunque, il EDT non viene mai bloccato dai metodi della coda bloccante.

Altre implementazioni

In aggiunta alla ArrayBlockingQuue usata in questo test, la libreria Java fornisce altre implementazioni della interfaccia BlockingQueue:

DelayQueue

Una coda bloccante illimitata di elementi delayed, in cui un elemento diventa generalmente idoneo per la rimozione quando il suo tempo è scaduto. Un elemento è considerato scaduto quando il suo metodo getDelay restituisce un valore minore o uguale a zero.
Un elemento è considerato la testa della coda se è l'elemento con il tempo di scadenza più breve, sia nel passato che nel futuro, se esiste un tale elemento. Un elemento è considerato la testa scaduta della coda se è l'elemento scaduto con il tempo di scadenza più breve nel passato, se esiste un tale elemento. La testa scaduta, quando presente, è anche la testa.

LinkedBlockingQueue

Una coda bloccante facoltativamente limitata basata su nodi collegati. Questa coda ordina gli elementi FIFO (first-in-first-out) e la testa della coda è quell'elemento che è stato nella coda per più tempo. La coda della coda è quell'elemento che è stato nella coda per meno tempo. I nuovi elementi vengono inseriti in coda e le operazioni di recupero restituiscono gli elementi dalla testa.
Le code collegate in genere hanno una produttività maggiore rispetto alle code basate su array, ma prestazioni meno prevedibili nella maggior parte delle applicazioni simultanee. L'argomento costruttore facoltativo con limite di capacità serve come un modo per impedire un'espansione eccessiva della coda. La capacità, se non specificata, è uguale a Integer.MAX_VALUE. I nodi collegati vengono creati dinamicamente a ogni inserimento, a meno che ciò non porti la coda oltre la capacità.

PriorityBlockingQueue

Una coda bloccante illimitata che utilizza le stesse regole di ordinamento della classe PriorityQueue e fornisce operazioni di recupero bloccanti. Sebbene questa coda sia logicamente illimitata, i tentativi di aggiunta potrebbero fallire a causa dell'esaurimento delle risorse (causando OutOfMemoryError). Questa classe non consente elementi nulli. Una coda di priorità che si basa sull'ordinamento naturale non consente inoltre l'inserimento di oggetti non confrontabili (ciò si traduce in ClassCastException). Questa classe e il suo iteratore implementano tutti i metodi opzionali delle interfacce Collection e Iterator. L'iteratore fornito nel metodo iterator() e lo Spliterator fornito nel metodo spliterator() non sono garantiti per attraversare gli elementi della PriorityBlockingQueue in un ordine particolare.

Le operazioni su questa classe non garantiscono l'ordinamento degli elementi con pari priorità. Se hai bisogno di imporre un ordinamento, puoi definire classi personalizzate o comparatori che utilizzano una chiave secondaria per rompere i pareggi nei valori di priorità primaria.

SynchronousQueue

Una coda bloccante in cui ogni operazione di inserimento deve attendere una corrispondente operazione di rimozione da parte di un altro thread e viceversa. Una coda sincrona non ha alcuna capacità interna, nemmeno una capacità di uno. Non puoi ispezionare una coda sincrona perché un elemento è presente solo quando provi a rimuoverlo; non puoi inserire un elemento (utilizzando qualsiasi metodo) a meno che un altro thread non stia tentando di rimuoverlo; non puoi iterare perché non c'è nulla da iterare. La testa della coda è l'elemento che il primo thread di inserimento in coda sta tentando di aggiungere alla coda; se non c'è tale thread in coda, nessun elemento è disponibile per la rimozione ed il metodo poll restituirà null.
Questa classe supporta una politica di equità facoltativa per ordinare i thread di produttori e consumatori in attesa. Per impostazione predefinita, questo ordinamento non è garantito. Tuttavia, una coda costruita con equità impostata su true concede l'accesso ai thread in ordine FIFO.

Questa classe e il suo iteratore implementano tutti i metodi facoltativi delle interfacce Collection e Iterator.

Ulteriore documentazione

La documentazione completa del progetto descritto in questo capitolo può essere visualizzata clikkando il seguente link: Le code bloccanti

Argomento precedente - Argomento successivo - Indice Generale