mattone
dopo mattoneLA COLLANA
IT/EN
← Guida Java

Java 25 · 35/39

Attività C20A — Operazioni atomiche

Java 25 · Guida completa · Bozza in revisione

Questa guida conserva lo stato di revisione del libro. La verifica editoriale e le prove di comprensione con lettori indipendenti sono ancora da completare.

Cerca in tutta la guida →

Queste attività accompagnano la stesura C20A. I programmi usano soltanto API stabili del JDK 25. Ogni risposta deve dichiarare l'invariante o la proprietà da garantire prima di scegliere una classe: un'uscita fortunata non sostituisce la spiegazione. Le attività restano in revisione finché i gate EV0–EV5 non saranno verificati con lettori.

C20A-A01 — Due richieste, una copia

Problema. Nel programma delle prenotazioni, immagina che A e B leggano entrambi 1. Scrivi i risultati possibili dei due compareAndSet(1, 0) e il valore finale. Ripeti il ragionamento se B arriva soltanto dopo il CAS riuscito di A. Poi compila ed esegui il programma più volte. Non tentare di prevedere quale lettore otterrà la copia.

Criterio di accettazione. La risposta deve mostrare che al massimo uno dei due CAS può restituire true per quella transizione, che un fallimento lascia il valore invariato e che il chiamante fallito rilegge prima di decidere. L'output del programma completo è cinque prenotazioni accettate e zero copie disponibili.

Suggerimento. Cerca il punto esatto in cui lo stato passa da 1 a 0. La lettura precedente non è ancora una prenotazione.

Soluzione ragionata. Se entrambi osservano 1, il primo CAS che effettivamente sostituisce 1 con 0 restituisce true. L'altro trova 0 e restituisce false; al giro successivo legge 0 e il metodo risponde false. Se B arriva dopo il successo di A, legge direttamente 0 e non chiama CAS. In nessuno dei due ordini il numero diventa negativo. Il conteggio esterno delle risposte true serve a controllare l'esito della prova, non a decidere la prenotazione.

C20A-P01 — Un nome non basta a garantire un ordine

Problema. Modifica una copia di PrenotazioniAtomiche affinché prenota() restituisca un numero da 1 a 5 per le richieste accettate e 0 per quelle respinte. Il numero deve indicare la posizione ottenuta nell'insieme delle cinque copie: quando un CAS sostituisce osservate con osservate - 1, la posizione è 6 - osservate. Raccogli i numeri ottenuti da venti lavoratori e verifica, dopo le join, che l'insieme sia esattamente {1, 2, 3, 4, 5}. Spiega perché questa posizione non è un identificatore persistente.

Criterio di accettazione. Nessun numero accettato si ripete; non compare alcun valore fuori dall'intervallo 1–5; quindici richieste ricevono 0. La risposta distingue questa numerazione in memoria da una chiave che debba restare unica dopo un riavvio o fra più processi.

Suggerimento. Restituisci la posizione soltanto nel ramo in cui il CAS riesce. Un numero calcolato prima può diventare obsoleto.

Soluzione ragionata. Nel ramo if (compareAndSet(...)) si restituisce 6 - osservate; quando osservate vale 5, 4, 3, 2, 1, i numeri prodotti sono rispettivamente 1, 2, 3, 4, 5. Un CAS fallito non assegna una posizione e riparte. La raccolta concorrente può usare una coda thread safe o un array con indice ottenuto atomicamente, ma la verifica si fa dopo le join. Un nuovo processo ricomincerebbe da cinque copie iniziali: l'archivio persistente deve definire un'altra garanzia per l'identità durevole.

C20A-E01 — L'effetto che può ripetersi

Problema. Nel programma di aggiornamento, qualcuno propone questa lambda: valore -> { inviaEmail(); return Math.min(10, valore + 2); }. Spiega perché non è corretta anche se il valore finale rimane entro dieci. Descrivi una soluzione per cui la conferma venga inviata dopo la decisione, indicando quale fallimento rimane possibile fra aggiornamento e invio.

Criterio di accettazione. La risposta identifica il possibile ricalcolo della funzione, l'invio duplicato e il nuovo problema «stato cambiato, email non inviata». Non deve affermare che spostare semplicemente la chiamata dopo updateAndGet garantisca una transazione fra memoria e servizio di posta.

Suggerimento. Una funzione passata a un aggiornamento atomico calcola un candidato; non rappresenta necessariamente una sola esecuzione dell'intera richiesta.

Soluzione ragionata. Se un altro thread aggiorna il valore prima che il tentativo termini, la funzione può essere invocata di nuovo. L'email della prima invocazione non può essere ritirata dal CAS fallito. Inviare dopo un aggiornamento riuscito evita l'invio per ogni tentativo fallito, ma resta la finestra in cui il programma può fermarsi prima dell'invio. Per una conferma affidabile serve un protocollo applicativo che registri decisione e messaggio da inviare nello stesso sistema persistente, oppure un meccanismo equivalente con recupero e deduplicazione.

C20A-T01 — Dalla prenotazione alla statistica

Problema. Una pagina registra molte visualizzazioni. Il rapporto finale viene letto solo dopo che tutti i lavoratori sono terminati; un pannello può mostrare durante il lavoro una stima aggiornata. Confronta AtomicLong e LongAdder. Poi cambia il requisito: ogni visualizzazione deve ricevere un numero progressivo distinto nel momento in cui arriva. Spiega perché la scelta può cambiare. Esegui StatisticheAccessi e indica che cosa rende esatto il suo output finale.

Criterio di accettazione. La soluzione distingue throughput sotto contesa e precisione della lettura concorrente. Riconosce che LongAdder.sum() durante gli aggiornamenti non è uno snapshot atomico, mentre dopo le join dell'esempio il totale è 8.000. Per assegnare un numero distinto al chiamante propone un'operazione atomica che restituisce il valore precedente o nuovo, con gestione dell'intervallo e della persistenza se richiesta.

Suggerimento. Chiediti se il chiamante debba conoscere il numero assegnato a questa richiesta oppure solo un totale collettivo.

Soluzione ragionata. Per il solo rapporto finale LongAdder è una scelta da misurare se gli aggiornamenti concorrenti sono numerosi; AtomicLong resta corretto e può essere più semplice. Nel programma, ogni lavoratore termina prima del sum(), quindi nessuno aggiorna mentre si forma il risultato letto da main. Quando invece ciascun chiamante deve ricevere un numero progressivo distinto, AtomicLong.getAndIncrement() esprime direttamente l'assegnazione locale. Né questa classe né LongAdder garantiscono da soli un numero unico fra più JVM o dopo il riavvio.

C20A-T02 — Qual è il confine dello stato?

Problema. Nel programma con stato immutabile la somma di disponibili e prenotati resta cinque. Disegna la sequenza di un lettore che prende un riferimento prima di un CAS riuscito e di un altro che lo prende dopo. Ora aggiungi un requisito: salvare su database il nome del lettore e la copia assegnata prima di confermare la richiesta. Quale parte può ancora essere gestita da AtomicReference e quale richiede un'altra garanzia?

Criterio di accettazione. La risposta indica le due fotografie valide (5, 0) e (4, 1) e spiega perché non compare (4, 0) se tutti leggono un solo Stato immutabile. Deve riconoscere che AtomicReference governa soltanto la fotografia nella JVM; la conferma persistente richiede una transazione o un protocollo equivalente nell'archivio.

Suggerimento. Un record è immutabile soltanto se anche le parti mutabili a cui rimanda sono gestite in modo coerente. Nel programma proposto i due componenti sono int, quindi non nascondono una lista modificabile.

Soluzione ragionata. Un lettore che chiama fotografia() prima della sostituzione riceve l'oggetto (5, 0); uno che lo chiama dopo riceve (4, 1). La variabile atomica pubblica un riferimento alla volta, e nessuno dei due record cambia. Aggiungere un nome a un database porta l'invariante oltre quella variabile: il CAS può riuscire e la scrittura fallire. La conferma deve seguire il contratto dell'archivio e trattare concorrenza, errori e ripetizioni a quel livello.

Prova gli esempi

Per eseguire i programmi serve JDK 25. Puoi scaricare i singoli file Java collegati nel capitolo oppure il progetto completo, che contiene istruzioni e uno script di avvio. Le spiegazioni confrontano anche l’output atteso: prevedilo prima di eseguire il programma.

Massimiliano Tarquini · CC BY-NC 4.0

Torna all’inizio ↑