Article image
Diego Vieira
Diego Vieira02/07/2026 02:35
Compartilhe

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 String comum 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!

Compartilhe
Comentários (0)