Java StringBuffer
Utilisez la classe StringBuffer thread-safe en Java pour les chaînes mutables partagées entre plusieurs threads.
StringBuffer est le grand frère de StringBuilder. Ils partagent une API, un parent commun (AbstractStringBuilder) et une implémentation basée sur un tampon d'octets. La seule différence est que les méthodes de StringBuffer sont synchronized — chaque append, insert, delete et toString acquiert un moniteur sur le tampon pendant la durée de l'appel. Cela rend un StringBuffer sûr à partager entre plusieurs threads. Cela rend également chaque opération plus lente que l'équivalent sur StringBuilder.
La bibliothèque de classes Java originale ne livrait que StringBuffer. StringBuilder est apparu dans JDK 1.5 (2004) précisément parce que la surcharge de la synchronisation posait problème dans le cas courant monothread. Depuis deux décennies, StringBuilder est le choix par défaut et StringBuffer le choix de niche.
Quand l'utiliser vraiment
D'abord, le résumé honnête : presque jamais. La liste des cas d'utilisation réels est courte, et la plupart d'entre eux ont de meilleures réponses en Java moderne.
Un StringBuffer n'est le bon choix que lorsque toutes ces conditions sont réunies :
- Un tampon est réellement partagé entre plusieurs threads.
- Chaque thread y ajoute ou insère du contenu indépendamment.
- Une
Stringimmuable finale est ce dont le consommateur a besoin à la fin. - Vous ne pouvez pas facilement restructurer le code pour que chaque thread construise son propre
StringBuilderet qu'un coordinateur les assemble.
Dans la plupart des codes concurrents, le dernier point est la porte de sortie : donnez à chaque thread son propre StringBuilder, renvoyez les chaînes, concaténez à la fin. Cela évite la contention sur un seul moniteur et supprime la surcharge de synchronisation du chemin critique.
Le cas restant — un petit nombre de threads traçant dans un tampon de diagnostic partagé, un journal d'audit alimenté par plusieurs acteurs — est là où StringBuffer justifie encore son existence.
L'API reflète celle de StringBuilder
Chaque mutateur de StringBuilder existe sur StringBuffer avec la même signature et le même type de retour. Les deux classes étendant AbstractStringBuilder, elles sont des jumeaux comportementaux ; la seule différence est le verrouillage :
StringBuffer sb = new StringBuffer(64);
sb.append("Hello, ")
.append("world")
.insert(0, "[INFO] ")
.append('!');
String out = sb.toString();Les constructeurs, length(), capacity(), charAt, substring, indexOf, reverse, delete, replace, setLength, ensureCapacity, trimToSize — tous présents, tous renvoyant les mêmes types. La revue de code pour StringBuffer est essentiellement la même que pour StringBuilder, avec une note sur le moniteur détenu.
Ce que signifie la synchronisation ici
synchronized sur les méthodes d'instance verrouille sur l'objet tampon lui-même. Ainsi :
StringBuffer log = new StringBuffer();
// Thread A
log.append("hit /users\n");
// Thread B (concurrently)
log.append("hit /orders\n");Chaque append s'exécute atomiquement par rapport à l'autre — aucun thread ne va altérer les octets de l'autre. Le résultat sera "hit /users\nhit /orders\n" ou "hit /orders\nhit /users\n". Il ne sera pas "hit /uhit /ordserss\n\n". C'est la garantie.
La garantie ne s'étend pas au-delà des appels de méthode. Une séquence de deux appends n'est pas atomique :
log.append(level); // unlock
// ← another thread might append here
log.append(": ");
log.append(message);
log.append('\n');Un second thread peut insérer une écriture entre les deux premiers appends et s'intercaler. Si vous souhaitez qu'un enregistrement complet soit déposé de manière contiguë, assemblez-le d'abord dans un StringBuilder local au thread, puis ajoutez la chaîne terminée en une seule fois dans le StringBuffer partagé. Ou — de manière équivalente — encapsulez l'écriture en plusieurs étapes dans un bloc synchronized (log) { ... } explicite.
Performance, brièvement
Chaque appel verrouillé paie le coût du moniteur : dans HotSpot moderne, c'est bon marché via le chemin rapide du verrou biaisé quand il n'y a pas de contention, et nettement plus coûteux sous contention. Par rapport à StringBuilder, un seul append sans contention est au plus légèrement plus lent d'un facteur constant. Sous contention, c'est dramatiquement plus lent, car les threads bloquent en attendant le moniteur.
La leçon à retenir : la contention est le coût, pas la primitive de verrouillage elle-même. Si vous avez mesuré un StringBuffer chaud et très sollicité, la bonne solution est de restructurer le code pour que chaque thread construise sa propre partie — pas de continuer à régler le tampon.
Un exemple concret
Le programme ci-dessous démarre un petit pool de threads d'écriture qui partagent un StringBuffer et ajoutent des lignes de marquage. Les méthodes synchronisées maintiennent chaque ligne intacte ; l'écriture délibérée en deux étapes montre pourquoi vous avez parfois encore besoin d'un bloc synchronized externe pour maintenir un groupe d'écritures ensemble.
Examinez la sortie d'exécution et vous verrez deux schémas. Les marqueurs [T?:i] sont individuellement intacts — aucune déchirure de balise — car chacun est un seul appel append. Mais l'ordre dans lequel les marqueurs de différents threads apparaissent est entrelacé. Les trois lignes -- end of T? --, en revanche, atterrissent chacune de manière contiguë, car le bloc synchronized (shared) { ... } externe maintient le verrou sur les quatre appends de ce groupe.
La suite
Cela couvre l'assemblage de chaînes mutables dans les deux variantes. La prochaine préoccupation est la production de chaînes pour l'affichage : rendre des nombres avec une largeur fixe, formater des dates, rembourrer des colonnes. Continuez vers le formatage de chaînes Java.