1. Threads clássicas: como isso funciona e onde dói
Vamos primeiro relembrar como funcionam os threads “normais” em Java (também chamados de threads de plataforma ou nativos). Aqueles que são criados via new Thread(...).
Quando você chama new Thread(() -> { ... }).start();, a JVM não apenas inicia um pedaço de código. Ela pede ao sistema operacional para criar um thread real de execução. O SO aloca para ele uma pilha separada (geralmente alguns megabytes) e reserva outros recursos de serviço.
Esse thread vive enquanto sua tarefa é executada e, durante todo esse tempo, ocupa um lugar na tabela de threads do sistema operacional. Quanto mais threads desse tipo, mais memória vai para suas pilhas e maior a carga no SO. É por isso que, com um grande número de threads em execução simultaneamente, o aplicativo pode começar a “sufocar” — o sistema gasta tempo demais alternando entre eles.
Exemplo: thread clássico
Thread thread = new Thread(() -> {
System.out.println("Olá da thread!");
});
thread.start();
Parece simples, não é? Mas tente criar não um ou dois, e sim, digamos, dez mil desses threads — e seu programa vai começar a sufocar rapidamente. Ou a memória se esgota, ou o sistema diz que o limite de threads foi atingido. Isso não é um erro do Java, mas uma consequência natural da arquitetura: threads são algo pesado e caro.
Por que isso acontece? Cada thread recebe sua própria pilha (geralmente 1–2 megabytes), além de um conjunto de estruturas de serviço do sistema operacional. E o SO também não fica feliz quando lhe apresentam dezenas de milhares de threads — ele tem limitações, às vezes bem rígidas.
E mesmo que a memória não acabe, surge outro problema — a troca de contexto. Quando há threads demais, o sistema fica pulando entre eles, salvando e restaurando seus estados. Tudo isso leva tempo e consome desempenho, de modo que o “paralelismo em massa” não traz o ganho esperado.
O problema “um thread — uma requisição”
Em aplicativos de servidor antigos (por exemplo, em Tomcat ou Jetty) frequentemente se usava o modelo “thread‑per‑request”: para cada requisição de usuário, era alocado um thread separado. Isso é conveniente, mas se você tem 10_000 usuários, você precisa de 10_000 threads! O servidor fica pesado e começa uma corrida por memória, não por velocidade de processamento.
Conclusão:
Threads clássicas são boas para um número pequeno de tarefas em paralelo, mas não escalam para dezenas ou centenas de milhares.
2. O que são threads virtuais (Virtual Threads)?
É aqui que entra o protagonista da aula de hoje — threads virtuais. Não é apenas “mais um thread”, mas uma ideia arquitetural completamente diferente.
Threads virtuais são threads gerenciados não pelo sistema operacional, mas pela própria JVM. Eles são implementados totalmente dentro do Java e podem ser criados em grandes quantidades (dezenas e centenas de milhares) sem “inchar” a memória nem causar lentidão.
Resumindo:
- Platform Thread (thread de plataforma): thread comum que corresponde diretamente a um thread do SO.
- Virtual Thread (thread virtual): thread leve, gerenciado pela JVM, não pelo SO.
Como funciona?
Threads virtuais são threads “leves” que vivem não no sistema operacional, mas dentro da própria JVM. Eles funcionam sobre um pequeno pool de threads reais, chamados de platform threads. Dá para imaginar que a JVM os gerencia como um maestro: ele tem um número limitado de músicos (threads reais), mas distribui habilmente as partituras (threads virtuais) entre eles.
Esquema arquitetural:
+-------------------+ +-------------------+
| Virtual Thread 1 |---\ | Platform Thread |
| Virtual Thread 2 |---->====> | (Carrier Thread) |
| Virtual Thread 3 |---/ +-------------------+
... (Sistema operacional)
Carrier Thread é um thread comum do SO no qual a JVM executa muitos threads virtuais. Se algum thread virtual de repente bloquear — por exemplo, esperando dados do disco ou da rede — a JVM simplesmente “congela” esse thread, liberando o carrier thread para outras tarefas.
Por que isso é revolucionário?
Porque agora é possível escrever código linear e habitual — sem callbacks infinitos, CompletableFuture e cadeias “infernais” de thenApply — e ao mesmo tempo escalar o aplicativo para milhares de operações simultâneas.
Threads virtuais consomem apenas dezenas de kilobytes (em vez de megabytes nos tradicionais) e são criados praticamente instantaneamente. Por isso, é possível iniciá-los e destruí-los aos milhares sem temer que o sistema operacional colapse sob o peso. Isso torna a programação paralela em Java finalmente leve e natural.
3. Vantagens dos threads virtuais
Escalabilidade
Com threads virtuais, você pode se dar ao luxo de iniciar dezenas e centenas de milhares de tarefas paralelas. Por exemplo, processar cada requisição de rede em um thread separado — sem se preocupar que o servidor “estoure”.
Demonstração: 100.000 threads virtuais
for (int i = 0; i < 100_000; i++) {
Thread.ofVirtual().start(() -> {
// Qualquer lógica pode ir aqui
try {
Thread.sleep(1000); // Simulação de trabalho
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
System.out.println("Todas as threads foram iniciadas!");
Esse código roda tranquilamente em um notebook comum!
Tente fazer o mesmo com threads tradicionais — você verá um OutOfMemoryError ou ganhará um “tijolo” no lugar do computador.
Simplicidade de programação
Threads virtuais permitem escrever o bom e velho código “bloqueante”, sem transformá-lo em um “espaguete” de chamadas assíncronas. Por exemplo, você pode usar tranquilamente Thread.sleep, InputStream.read, Socket.accept — a JVM se encarrega de não bloquear todo o carrier thread.
Melhoria da legibilidade e da manutenção do código
Em vez de esquemas complexos com callbacks e CompletableFuture, você escreve código linear e claro. Isso reduz a quantidade de bugs e facilita a manutenção.
Sem reinventar a roda
Antes, para processar milhares de requisições em paralelo, era preciso usar frameworks assíncronos, bibliotecas reativas (Netty, Vert.x, Project Reactor), que exigem um estilo específico de programação. Agora você pode passar sem eles — e ainda assim obter escalabilidade.
4. Arquitetura: como os threads virtuais funcionam “sob o capô”
Mapeamento para carrier threads
A JVM cria um pequeno pool de threads reais (carrier threads) — geralmente em quantidade igual ao número de núcleos do processador. Todos os threads virtuais “viajam” nesses carrier threads, como passageiros em ônibus.
- Quando um thread virtual bloqueia (por exemplo, esperando uma resposta da rede), a JVM o “descarrega” do carrier thread e o coloca em uma fila.
- Assim que o thread pode continuar, a JVM o “coloca” novamente em um carrier thread livre.
Analogia:
Imagine que você tem 4 táxis (carrier threads) e atende 10.000 clientes (virtual threads). Assim que um cliente chega e sai, o táxi imediatamente pega o próximo. Ninguém fica parado, e os táxis não “quebram” sob o peso dos passageiros.
Escalonamento e comutação
A JVM decide por conta própria qual thread virtual executar no momento. Se um thread bloqueia em I/O, ele não atrapalha os outros de trabalharem.
5. Limitações e particularidades dos threads virtuais
Nem tudo que é virtual é ouro
Não serve para cálculos longos: Se você tem uma tarefa que ocupa o processador o tempo todo (CPU‑bound pesado), um thread virtual não trará ganho de desempenho. Simplesmente porque os carrier threads ainda estão limitados pelo número de núcleos.
Alguns bloqueios não são eficientes: Mecanismos antigos de sincronização (por exemplo, sincronização via synchronized em objetos com mutexes nativos) podem impedir a JVM de “congelar” o thread virtual. Nesses casos, o carrier thread esperará junto com o thread virtual, reduzindo a escalabilidade.
Nem todas as bibliotecas se dão bem com threads virtuais: Se a biblioteca faz chamadas nativas ou usa bloqueios específicos, os threads virtuais podem não se comportar como o esperado.
Exemplo: quando não vale a pena usar Virtual Threads
Se você tem uma tarefa que roda em um loop infinito e fica calculando números, um thread virtual não trará nenhum ganho. Ainda esbarramos no número de núcleos.
Thread.ofVirtual().start(() -> {
while (true) {
// Contando até o infinito
}
});
Resultado:
Um carrier thread ficará ocupado com esse thread virtual, e outras tarefas aguardarão sua vez.
6. Comparação: Platform Thread vs Virtual Thread
| Característica | Platform Thread (Convencional) | Virtual Thread (Virtual) |
|---|---|---|
| Controlado por | Sistema operacional | JVM |
| Memória por thread | Megabytes | Dezenas de kilobytes |
| Quantidade de threads | Geralmente < 10_000 | Milhares, centenas de milhares |
| Custo de criação | Caro | Barato |
| Escalabilidade | Limitada | Quase ilimitada |
| Adequado para | Tarefas longas, CPU‑bound | Tarefas curtas e I/O‑bound |
| Comutação de threads | SO | JVM |
| Compatibilidade | 100% | Quase sempre, mas há nuances |
7. Exemplo: como seria o servidor antes e depois de Virtual Threads
Antes (Platform Threads)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
new Thread(() -> handleClient(client)).start();
}
Problema:
Após 5.000 conexões, o servidor começará a “engasgar”.
Depois (Virtual Threads, Java 21+)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
Thread.ofVirtual().start(() -> handleClient(client));
}
Magia:
Agora é possível processar dezenas de milhares de conexões — sem pensar em limites de threads!
9. Erros comuns ao migrar para threads virtuais
Erro nº 1: Esperar aceleração para tarefas de computação. Threads virtuais não aceleram tarefas que ocupam totalmente o processador. Para tais tarefas, ainda esbarramos no número de núcleos.
Erro nº 2: Uso de sincronizações bloqueantes antigas. Se você usa bloqueios antigos (por exemplo, synchronized em objetos que podem ser “travados” nativamente), os threads virtuais podem não ser descarregados do carrier thread, perdendo todas as vantagens.
Erro nº 3: Não considerar o comportamento de bibliotecas de terceiros. Algumas bibliotecas de terceiros podem não estar prontas para trabalhar com threads virtuais (por exemplo, se usam JNI ou bloqueios nativos).
Erro nº 4: Esperar um aumento mágico de desempenho. Threads virtuais não são panaceia. Eles não aceleram tudo; apenas tornam o paralelismo barato e conveniente para tarefas I/O‑bound.
GO TO FULL VERSION