StringBuilder vs StringBuffer: O dia em que meu código sumiu com 8.5 milhões de dados
- #Java
- #Data
"Sou rápido, só não disse que sou bom!" 🤣
Essa piada resume perfeitamente o teste que fiz hoje e que mudou minha forma de enxergar a manipulação de textos em Java.
Estou maratonando o Bootcamp de Java Back-End com IA do Santander na DIO e, durante a aula sobre a imutabilidade de String, resolvi sair um pouco da teoria. Decidi criar um ambiente de teste de estresse severo em multithread para ver a diferença real de comportamento entre String, StringBuilder e StringBuffer.
🛑 O Primeiro Choque: A String Comum
Antes mesmo de envolver múltiplas threads, tentei rodar um loop simples de 10 milhões de iterações com uma String comum concatenando caracteres.
Resultado: Inviável. Por ser imutável, o Java tenta criar bilhões de objetos intermediários na memória heap, fazendo a performance despencar e quase travando a máquina. Descartada logo de cara por falta de compatibilidade com alta performance!
⚔️ O Teste de Estresse: 10 Threads vs 1 Objeto
Para o teste real, configurei 10 threads paralelas, onde cada uma delas tinha a missão de inserir 1 milhão de caracteres "X" no mesmo objeto de texto simultaneamente (totalizando 10 milhões de caracteres esperados).
Se você quiser rodar na sua máquina para ver o caos acontecer, utilizei esta lógica:
Java
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws InterruptedException {
int numeroDeThreads = 10;
int repeticoesPorThread = 1_000_000; // Cada thread vai dar 1 milhão de appends
// ==========================================
// TESTE 1: STRINGBUFFER
// ==========================================
var bufferConcat = new StringBuffer();
var bufferStart = OffsetDateTime.now();
List<Thread> threadsBuffer = new ArrayList<>();
for (int i = 0; i < numeroDeThreads; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < repeticoesPorThread; j++) {
bufferConcat.append("X");
}
});
threadsBuffer.add(t);
t.start();
}
// Espera todas as threads terminarem
for (Thread t : threadsBuffer) t.join();
var bufferEnd = OffsetDateTime.now();
System.out.printf("StringBuffer -> Tempo: %s ms | Tamanho Final Esperado: 10000000 | Tamanho Real: %d\n",
Duration.between(bufferStart, bufferEnd).toMillis(), bufferConcat.length());
// ==========================================
// TESTE 2: STRINGBUILDER
// ==========================================
var builderConcat = new StringBuilder();
var builderStart = OffsetDateTime.now();
List<Thread> threadsBuilder = new ArrayList<>();
for (int i = 0; i < numeroDeThreads; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < repeticoesPorThread; j++) {
builderConcat.append("X");
}
});
threadsBuilder.add(t);
t.start();
}
// Espera todas as threads terminarem
for (Thread t : threadsBuilder) t.join();
var builderEnd = OffsetDateTime.now();
System.out.printf("StringBuilder -> Tempo: %s ms | Tamanho Final Esperado: 10000000 | Tamanho Real: %d\n",
Duration.between(builderStart, builderEnd).toMillis(), builderConcat.length());
}
}
📊 Os Resultados na Prática
Ao rodar o código, os logs no meu terminal evidenciaram o perigo real da falta de sincronização:
- StringBuilder: Terminou em incríveis 149 ms! O problema? Dos 10.000.000 de caracteres esperados, ele só gravou 1.428.684. Ele foi absurdamente rápido, mas entregou o resultado completamente errado. Como não é Thread-Safe, as threads acessaram a memória ao mesmo tempo, gerando uma Race Condition onde uma atropelou e apagou os dados da outra.
- StringBuffer: Demorou bem mais (662 ms), mas entregou cravado: 10.000.000 de caracteres. Por possuir métodos sincronizados (
synchronized), ele funciona como um "cadeado" na memória, garantindo que apenas uma thread altere o buffer por vez.
💡 Lição Prática para os Projetos
- Em loops Single-Thread (99% do dia a dia): Esqueça a
Stringcomum e use StringBuilder. Como não há disputa de threads dentro do mesmo método, ele vai voar em performance sem risco de corromper seus dados. - Em ambientes Multi-Thread reais: Se o seu sistema tiver concorrência real de threads alterando a mesma referência de texto, o StringBuffer é obrigatório para garantir que seu relatório não perca 80% dos dados no limbo da memória.
🌐 Mas quando é que usamos múltiplas threads na vida real?
Para quem está começando, pensar em "threads brigando por texto" parece abstrato, mas pense em uma Planilha Compartilhada (como o Google Sheets):
Imagine que você e um colega estão editando a mesma célula C3 exatamente no mesmo milissegundo. Você digita "Banana" e ele digita "Maçã".
- Se o sistema funcionasse como o StringBuilder (sem travas), a célula exibiria um texto totalmente corrompido (como
Maçanana) ou a planilha inteira travaria. - Como o sistema é sincronizado (da mesma forma que o StringBuffer), o servidor cria uma fila justa de microssegundos. Ele processa um texto de cada vez de forma limpa. O último a chegar substitui o primeiro, mas os dados permanecem consistentes e legíveis, sem quebrar a aplicação.
Estudar é exatamente isso: pausar a aula, codar seu próprio teste e ver o comportamento real da JVM acontecer na tela! 🚀
Espero que esse teste ajude quem está na trilha de Java a entender esse conceito de forma bem visual. Se rodarem o código aí, me contem nos comentários quantos dados o StringBuilder de vocês engoliu!



