Java StringBuffer
Usa la classe StringBuffer thread-safe in Java per stringhe mutabili condivise tra thread.
StringBuffer è il fratello maggiore di StringBuilder. Condividono un'API, condividono un genitore (AbstractStringBuilder) e condividono un'implementazione basata su buffer di byte. L'unica differenza è che i metodi di StringBuffer sono synchronized — ogni append, insert, delete e toString acquisisce un monitor sul buffer per la durata della chiamata. Questo rende StringBuffer sicuro da condividere tra thread. Rende anche ogni operazione più lenta rispetto all'equivalente su StringBuilder.
La libreria di classi Java originale includeva solo StringBuffer. StringBuilder è arrivato con JDK 1.5 (2004) proprio perché il costo della sincronizzazione rappresentava un problema nel caso comune a singolo thread. Negli ultimi vent'anni, StringBuilder è diventato la scelta predefinita e StringBuffer quella di nicchia.
Quando usarlo davvero
Prima la sintesi onesta: quasi mai. L'elenco dei casi d'uso reali è breve, e la maggior parte di essi ha risposte migliori nel Java moderno.
StringBuffer è la scelta giusta solo quando ognuna di queste condizioni è vera:
- Un buffer è genuinamente condiviso tra più thread.
- Ogni thread aggiunge o inserisce contenuto in modo indipendente.
- Un
Stringimmutabile finale è ciò di cui il consumatore ha bisogno alla fine. - Non è possibile ristrutturare facilmente il codice in modo che ogni thread costruisca il proprio
StringBuildere un coordinatore li unisca.
Nella maggior parte del codice concorrente, l'ultimo punto è la via d'uscita: dai a ogni thread il proprio StringBuilder, restituisci le stringhe, concatenale alla fine. Questo evita la contesa su un singolo monitor e rimuove il costo della sincronizzazione dal percorso critico.
Il caso rimanente — un numero ridotto di thread che scrivono in un buffer diagnostico condiviso, un log di audit su cui più attori aggiungono dati — è quello in cui StringBuffer si guadagna ancora il suo posto.
L'API rispecchia StringBuilder
Ogni metodo mutante di StringBuilder esiste in StringBuffer con la stessa firma e lo stesso tipo di ritorno. Poiché entrambe le classi estendono AbstractStringBuilder, sono gemelli comportamentali; l'unica differenza è il locking:
StringBuffer sb = new StringBuffer(64);
sb.append("Hello, ")
.append("world")
.insert(0, "[INFO] ")
.append('!');
String out = sb.toString();Costruttori, length(), capacity(), charAt, substring, indexOf, reverse, delete, replace, setLength, ensureCapacity, trimToSize — tutti presenti, tutti restituiscono gli stessi tipi. La revisione del codice per StringBuffer è essenzialmente la stessa di quella per StringBuilder, con l'aggiunta di una nota su quale monitor viene acquisito.
Cosa significa la sincronizzazione qui
synchronized sui metodi di istanza blocca sull'oggetto buffer stesso. Quindi:
StringBuffer log = new StringBuffer();
// Thread A
log.append("hit /users\n");
// Thread B (concurrently)
log.append("hit /orders\n");Ogni append viene eseguito atomicamente rispetto all'altro — nessun thread corrompere i byte dell'altro. Il risultato sarà "hit /users\nhit /orders\n" oppure "hit /orders\nhit /users\n". Non sarà "hit /uhit /ordserss\n\n". Questa è la garanzia.
La garanzia non si estende attraverso più chiamate ai metodi. Una sequenza di due append non è atomica:
log.append(level); // unlock
// ← another thread might append here
log.append(": ");
log.append(message);
log.append('\n');Un secondo thread può inserire una scrittura tra i primi due append e intercalare. Se vuoi che un intero record venga scritto in modo contiguo, assembla il contenuto in un StringBuilder locale al thread e poi aggiungi la stringa completata in una sola operazione al StringBuffer condiviso. Oppure — equivalentemente — racchiudi la scrittura in più passi in un blocco esplicito synchronized (log) { ... }.
Performance, in breve
Ogni chiamata con lock paga il costo del monitor: in HotSpot moderno questo è economico tramite biased-lock-fast-path quando non c'è contesa, e notevolmente più costoso sotto contesa. Rispetto a StringBuilder, un singolo append non conteso è al massimo un piccolo fattore costante più lento. Sotto contesa è drammaticamente più lento, perché i thread si bloccano in attesa del monitor.
Il messaggio chiave: la contesa è il costo, non la primitiva di locking in sé. Se hai misurato un StringBuffer caldo e conteso, la soluzione giusta è ristrutturare il codice affinché ogni thread costruisca la propria parte — non ottimizzare ulteriormente il buffer.
Un esempio pratico
Il programma seguente avvia un piccolo pool di thread che condividono un StringBuffer e aggiungono righe marcate. I metodi sincronizzati mantengono ogni riga intatta; la scrittura intenzionale in due passi dimostra perché a volte è ancora necessario un blocco synchronized esterno per mantenere insieme un gruppo di scritture.
Osservando l'output dell'esecuzione vedrai due pattern. I marcatori [T?:i] sono individualmente intatti — senza byte corrotti — perché ognuno è una singola chiamata append. Ma l'ordine in cui i marcatori di thread diversi appaiono è intercalato. Le tre righe -- end of T? --, al contrario, arrivano ciascuna in modo contiguo, perché il blocco esterno synchronized (shared) { ... } mantiene il lock attraverso tutti e quattro gli append per quel gruppo.
Cosa viene dopo
Questo copre l'assemblaggio di stringhe mutabili in entrambe le varianti. Il tema successivo riguarda la produzione di stringhe per la visualizzazione: renderizzare numeri con larghezza fissa, formattare date, allineare colonne. Continua con Java String formatting.