Java StringBuffer
Verwende die threadsichere StringBuffer-Klasse in Java für veränderliche Strings, die zwischen Threads geteilt werden.
StringBuffer ist das ältere Geschwister von StringBuilder. Beide teilen eine API, beide teilen eine gemeinsame Elternklasse (AbstractStringBuilder), und beide teilen eine Byte-Puffer-Implementierung. Der einzige Unterschied besteht darin, dass die Methoden von StringBuffer synchronized sind — jedes append, insert, delete und toString erwirbt für die Dauer des Aufrufs einen Monitor auf dem Puffer. Dadurch ist ein StringBuffer sicher für den gemeinsamen Einsatz über mehrere Threads hinweg. Das macht aber auch jede Operation langsamer als die entsprechende Operation auf StringBuilder.
Die ursprüngliche Java-Klassenbibliothek enthielt nur StringBuffer. StringBuilder kam mit JDK 1.5 (2004) genau deshalb, weil der Synchronisationsaufwand im häufigen Single-Thread-Fall ein Problem darstellte. In den letzten zwei Jahrzehnten ist StringBuilder die Standardwahl und StringBuffer die Nischenwahl.
Wann man ihn tatsächlich verwenden sollte
Zuerst die ehrliche Zusammenfassung: fast nie. Die Liste der echten Anwendungsfälle ist kurz, und die meisten davon haben bessere Antworten in modernem Java.
Ein StringBuffer ist die richtige Wahl nur dann, wenn alle dieser Bedingungen zutreffen:
- Ein Puffer wird tatsächlich über mehrere Threads hinweg gemeinsam genutzt.
- Jeder Thread fügt unabhängig voneinander Daten ein oder hängt sie an.
- Am Ende benötigt der Verbraucher einen endgültigen, einzelnen unveränderlichen
String. - Der Code lässt sich nicht leicht so umstrukturieren, dass jeder Thread seinen eigenen
StringBuilderaufbaut und ein Koordinator sie zusammenführt.
In den meisten nebenläufigen Code-Szenarien ist der letzte Punkt der Ausweg: Gib jedem Thread seinen eigenen StringBuilder, gib die Strings zurück, verknüpfe sie am Ende. Das vermeidet Konkurrenz auf einem einzelnen Monitor und entfernt den Synchronisationsaufwand aus dem kritischen Pfad.
Der verbleibende Anwendungsfall — eine kleine Anzahl von Threads, die in einen gemeinsamen Diagnosepuffer schreiben, ein Audit-Log, das von mehreren Akteuren befüllt wird — ist der Bereich, in dem StringBuffer noch seinen Platz hat.
Die API spiegelt StringBuilder wider
Jede Mutationsmethode von StringBuilder existiert auch bei StringBuffer mit der gleichen Signatur und dem gleichen Rückgabetyp. Da beide Klassen AbstractStringBuilder erweitern, sind sie verhaltenstechnisch identisch; der einzige Unterschied ist das Sperren:
StringBuffer sb = new StringBuffer(64);
sb.append("Hello, ")
.append("world")
.insert(0, "[INFO] ")
.append('!');
String out = sb.toString();Konstruktoren, length(), capacity(), charAt, substring, indexOf, reverse, delete, replace, setLength, ensureCapacity, trimToSize — alle vorhanden, alle geben die gleichen Typen zurück. Ein Code-Review für StringBuffer ist im Wesentlichen dasselbe wie ein Review für StringBuilder, ergänzt um einen Hinweis darauf, welcher Monitor gehalten wird.
Was Synchronisation hier bedeutet
synchronized auf Instanzmethoden sperrt auf dem Pufferobjekt selbst. Also:
StringBuffer log = new StringBuffer();
// Thread A
log.append("hit /users\n");
// Thread B (concurrently)
log.append("hit /orders\n");Jedes append läuft atomar in Bezug auf das andere — kein Thread wird die Bytes des anderen beschädigen. Das Ergebnis wird entweder "hit /users\nhit /orders\n" oder "hit /orders\nhit /users\n" sein. Es wird nicht "hit /uhit /ordserss\n\n" sein. Das ist die Garantie.
Die Garantie erstreckt sich nicht über mehrere Methodenaufrufe hinweg. Eine Folge von zwei append-Aufrufen ist nicht atomar:
log.append(level); // unlock
// ← another thread might append here
log.append(": ");
log.append(message);
log.append('\n');Ein zweiter Thread kann einen Schreibvorgang zwischen die ersten beiden append-Aufrufe einschieben und sich dazwischen drängen. Wenn ein ganzer Datensatz zusammenhängend landen soll, assembliere ihn zuerst in einem thread-lokalen StringBuilder und rufe dann einmal append mit dem fertigen String auf dem gemeinsamen StringBuffer auf. Oder — gleichwertig — umhülle den mehrstufigen Schreibvorgang mit einem expliziten synchronized (log) { ... }-Block.
Performance, kurz zusammengefasst
Jeder gesperrte Aufruf zahlt für den Monitor: In modernem HotSpot ist das bei fehlender Konkurrenz über den Biased-Lock-Fast-Path günstig, und spürbar teurer unter Konkurrenz. Im Vergleich zu StringBuilder ist ein einzelnes unstrittiges append höchstens um einen kleinen konstanten Faktor langsamer. Unter Konkurrenz ist es deutlich langsamer, da Threads auf den Monitor warten.
Die Erkenntnis: Konkurrenz ist der Kostenfaktor, nicht das Sperr-Primitiv selbst. Wenn du einen heißen, stark umkämpften StringBuffer gemessen hast, ist die richtige Lösung eine Umstrukturierung, sodass jeder Thread seinen eigenen Teil aufbaut — nicht eine weitere Optimierung des Puffers.
Ein ausgearbeitetes Beispiel
Das folgende Programm startet einen kleinen Pool von Writer-Threads, die einen StringBuffer gemeinsam nutzen und Markierungszeilen anhängen. Die synchronisierten Methoden halten jede Zeile intakt; der absichtlich zweistufige Schreibvorgang zeigt, warum man manchmal noch einen äußeren synchronized-Block benötigt, um eine Gruppe von Schreibvorgängen zusammenzuhalten.
Sieh dir die Ausgabe des Programms an und du wirst zwei Muster erkennen. Die [T?:i]-Markierungen sind einzeln intakt — keine beschädigten Tags — weil jede ein einzelner append-Aufruf ist. Aber die Reihenfolge, in der die Markierungen verschiedener Threads erscheinen, ist verschachtelt. Die drei -- end of T? ---Zeilen hingegen landen jeweils zusammenhängend, weil der äußere synchronized (shared) { ... }-Block die Sperre für alle vier append-Aufrufe dieser Gruppe hält.
Was als Nächstes kommt
Damit ist die veränderliche String-Assemblierung in beiden Varianten abgedeckt. Als Nächstes geht es darum, Strings für die Anzeige zu produzieren: Zahlen mit fester Breite darstellen, Daten formatieren, Spalten auffüllen. Weiter zu Java String-Formatierung.