Trilha de Kotlin · parte 22 de 59
Threads e concorrência na JVM
Threads, condição de corrida, synchronized, volatile, pools com Future e deadlock na JVM, com código rodado em Kotlin e as saídas que apareceram de verdade.
intermediárioPublicada em · 12 min de leitura
Threads e concorrência na JVM
Imagine uma cozinha de restaurante com três cozinheiros e uma única geladeira. Se dois pegam o último ovo ao mesmo tempo, um deles volta de mãos vazias sem saber por quê. Threads são esses cozinheiros, e a geladeira é a memória que eles compartilham. A analogia tem limite: cozinheiros combinam a vez conversando, e threads não combinam nada. Quem decide a vez é o sistema operacional. A aula usa funções, lambdas e propriedades das fases 1 a 3, e não supõe contato prévio com threads.
o mapa da aula
Vemos e , criamos threads e mostramos que a ordem entre elas não é garantida. Um contador de visitas mostra a e as formas de protegê-la. Fecham a aula o custo das threads, os erros comuns e dois exercícios.
1. processo e thread
Um processo é um programa em execução, com memória própria. Dentro dele, as threads avançam no seu ritmo e enxergam os mesmos objetos, então precisam se coordenar ao tocar neles. O guarda objetos e campos estáticos, como a propriedade de topo de arquivo que servirá de contador. A pilha é privada de cada thread e guarda as variáveis locais, que nunca são o problema desta aula.
As threads estão à esquerda e o heap à direita. Sem pontas de seta, o rótulo diz a direção: duas threads leem e escrevem visitas, e a main só lê no fim.
2. por que usar várias threads
Enquanto uma thread espera I/O (entrada e saída, como a resposta de uma conexão de rede), outra segue trabalhando. Além disso, uma thread avança num núcleo por vez, então dividir um cálculo permite usar vários núcleos. A primeira razão é a que as coroutines atacam, como mostra suspend e a máquina de estados.
3. criar uma thread e esperar por ela
Thread { } cria a thread sem iniciá-la, e start() pede ao sistema que a execute. thread { } faz as duas coisas, e join() espera a thread terminar. A pausa de 1 ms só mistura as linhas.
import kotlin.concurrent.thread
fun main() {
val a = Thread {
repeat(5) {
println("A passo $it")
Thread.sleep(1)
}
}
a.start()
val b = thread {
repeat(5) {
println("B passo $it")
Thread.sleep(1)
}
}
a.join()
b.join()
println("fim")
}
Duas execuções no JDK 21:
-- JDK 21, execução 1 --
A passo 0
A passo 1
A passo 2
B passo 0
A passo 3
B passo 1
A passo 4
B passo 2
B passo 3
B passo 4
fim
-- JDK 21, execução 2 --
A passo 0
A passo 1
A passo 2
A passo 3
A passo 4
B passo 0
B passo 1
B passo 2
B passo 3
B passo 4
fim
Em cinco execuções no JDK 21 saíram três padrões. No JDK 25 saíram todas iguais. A ordem é decisão do escalonador, e nenhum programa deve depender dela.
4. condição de corrida
O domínio da aula é um contador de visitas: duas threads somam um milhão de visitas cada, e o total certo seria 2 000 000.
import kotlin.concurrent.thread
var visitas = 0
fun main() {
val a = thread { repeat(1_000_000) { visitas++ } }
val b = thread { repeat(1_000_000) { visitas++ } }
a.join()
b.join()
println("visitas: $visitas (esperado: 2000000)")
}
Duas das dez execuções, uma por JDK:
-- JDK 21, execução 1 --
visitas: 1732221 (esperado: 2000000)
-- JDK 25, execução 1 --
visitas: 1935517 (esperado: 2000000)
Nas dez, o total variou de 1 417 101 a 2 000 000. Isso é uma condição de corrida (race condition): o resultado depende da intercalação, e o programa não reclama. A causa está em visitas++, que lê, soma e grava. Duas threads podem ler o mesmo 5 antes de qualquer gravação.
Na linha do tempo, as duas leem 5 e gravam 6, e um incremento se perde. Um dos dez acertou, no JDK 25, e o código seguia errado.
5. com synchronized
Exclusão mútua garante que só uma thread executa um trecho crítico por vez. A JVM usa um , o lock intrínseco de cada objeto. Pense na chave do banheiro de um posto de gasolina: quem pega a chave entra, e a fila espera. O limite é que a chave é física, e o monitor protege só quem pede o mesmo objeto. @Synchronized numa função de membro usa o monitor da instância, e synchronized(objeto) { } usa o objeto informado. A seção 6 usa as duas formas no mesmo programa. Um programa só com @Synchronized chegou a 2 000 000 em seis execuções, três por JDK.
A trava custa, porque quem chega fica em fila. Medi com duas threads de cinco milhões de chamadas cada, em três rodadas, em duas execuções por JDK: sem trava, de 13 a 73 ms; com @Synchronized, de 320 a 518 ms. A versão sem trava perdeu entre 28% e 39% dos incrementos, por isso não é um benchmark. Proteja o menor trecho possível.
6. dois locks diferentes
O erro mais comum é usar travas diferentes para o mesmo dado. @Synchronized trava this, e synchronized(trava) trava outro objeto, então as duas threads não se excluem.
import kotlin.concurrent.thread
class Contador {
private var visitas = 0
private val trava = Any()
@Synchronized
fun porMetodo() {
visitas++
}
fun porBloco() {
synchronized(trava) {
visitas++
}
}
fun total() = visitas
}
fun main() {
val contador = Contador()
val a = thread { repeat(1_000_000) { contador.porMetodo() } }
val b = thread { repeat(1_000_000) { contador.porBloco() } }
a.join()
b.join()
println("visitas: ${contador.total()} (esperado: 2000000)")
}
Uma execução por JDK:
-- JDK 21, execução 1 --
visitas: 1715654 (esperado: 2000000)
-- JDK 25, execução 1 --
visitas: 1586353 (esperado: 2000000)
Nas execuções feitas, os totais ficaram entre 1 495 895 e 1 728 740. Todo acesso ao mesmo estado precisa usar o mesmo monitor.
7. visibilidade com volatile
Sem sincronização, uma thread pode deixar de ver a gravação de outra. @Volatile torna as escritas no campo visíveis a todas, e a documentação do Kotlin diz que leituras e escritas do campo são atômicas. Isso não substitui a exclusão mútua, porque visitas++ continua sendo ler e depois gravar. Num teste com um laço que espera uma flag, sem @Volatile a thread continuou viva depois de 3 s em quatro execuções, e com a anotação o laço terminou em todas. O resultado depende do compilador JIT, e a documentação garante a visibilidade, não que o laço trave.
8. pools com ExecutorService e Future
Criar uma thread por tarefa tem custo, como a seção 10 mostra. Um pool mantém um número fixo de threads, que pegam tarefas de uma fila. submit recebe um Callable, uma tarefa que devolve um resultado, e devolve um Future, o resultado que ainda pode não existir. get() espera e devolve o valor. A analogia é um restaurante com dois garçons e uma fila de pedidos. O limite: o pool limita quantas tarefas rodam juntas, mas cada thread continua custando.
import java.util.concurrent.Callable
import java.util.concurrent.Executors
fun main() {
val pool = Executors.newFixedThreadPool(2)
val futuros = (1..4).map { n ->
pool.submit(Callable {
Thread.sleep(200)
println("tarefa $n na ${Thread.currentThread().name}")
n * n
})
}
val soma = futuros.sumOf { it.get() }
pool.shutdown()
println("soma dos quadrados: $soma")
}
Uma execução por JDK. A ordem nas threads muda, e a soma é sempre 30:
-- JDK 21, execução 1 --
tarefa 2 na pool-1-thread-2
tarefa 1 na pool-1-thread-1
tarefa 3 na pool-1-thread-2
tarefa 4 na pool-1-thread-1
soma dos quadrados: 30
-- JDK 25, execução 1 --
tarefa 2 na pool-1-thread-2
tarefa 1 na pool-1-thread-1
tarefa 4 na pool-1-thread-1
tarefa 3 na pool-1-thread-2
soma dos quadrados: 30
Sem shutdown(), a soma sai, mas o processo não termina. Rodei com limite de 6 s e encerrei na mão, nos dois JDKs. Pela documentação, shutdown() barra novas tarefas, as threads do pool padrão são não daemon, e a JVM só encerra quando restam apenas threads daemon.
9.
Deadlock é o ciclo em que duas threads seguram um lock cada uma e esperam o da outra. A analogia é o cruzamento estreito em que dois carros se encaram. O limite: carro pode dar ré, e uma thread presa num monitor não solta a trava. O código usa threads daemon e join com limite de 2 s, para não travar a máquina.
import kotlin.concurrent.thread
val trancaA = Any()
val trancaB = Any()
fun main() {
val t1 = thread(isDaemon = true) {
synchronized(trancaA) {
Thread.sleep(100)
synchronized(trancaB) { println("t1 terminou") }
}
}
val t2 = thread(isDaemon = true) {
synchronized(trancaB) {
Thread.sleep(100)
synchronized(trancaA) { println("t2 terminou") }
}
}
t1.join(2000)
t2.join(2000)
println("t1 viva: ${t1.isAlive}, t2 viva: ${t2.isAlive}")
val ciclo = java.lang.management.ManagementFactory.getThreadMXBean().findDeadlockedThreads()
println("threads em deadlock: ${ciclo?.size ?: 0}")
}
Duas execuções por JDK:
-- JDK 21, execução 1 --
t1 viva: true, t2 viva: true
threads em deadlock: 2
-- JDK 21, execução 2 --
t1 viva: true, t2 viva: true
threads em deadlock: 2
-- JDK 25, execução 1 --
t1 viva: true, t2 viva: true
threads em deadlock: 2
-- JDK 25, execução 2 --
t1 viva: true, t2 viva: true
threads em deadlock: 2
Nas quatro, as threads ficaram vivas e o ThreadMXBean achou o ciclo. Ele devolve null sem ciclo, por isso ?.size ?: 0.
Hexágonos azuis são threads, e retângulos laranja são locks. A linha azul diz o que cada thread segura, e a rosa tracejada, com o X, é o pedido barrado.
10. o custo das threads
Uma thread de plataforma ocupa memória para a sua pilha e entra no escalonamento do sistema operacional. Não medi a memória por thread. Medi só a criação: 10 000 threads dormindo 1 s terminaram em cerca de 4 s, nos dois JDKs, nesta máquina. Dez mil coroutines sobre uma thread aparecem em suspend e a máquina de estados, e a alternativa do Java 21 está em Virtual threads e testes com runTest.
Pergunta de entrevistaComo você evitaria um deadlock?
Deadlock acontece quando duas threads seguram locks e esperam o lock uma da outra. Evito pedindo os locks sempre na mesma ordem global, usando tryLock com timeout, e mantendo a seção crítica pequena. Para diagnosticar, uso um dump de threads, como jstack, ou o ThreadMXBean, que encontra o ciclo. Em código de aplicação, prefiro estruturas de alto nível, como filas e canais, para não gerenciar locks à mão.
11. erros comuns e boas práticas
Chamar run() em vez de start() não cria thread: o código roda na thread atual, e nenhuma mensagem avisa. A documentação diz que o método “is not intended to be invoked directly”. Os outros erros já têm seção: travas diferentes na 6, pool sem shutdown() na 8. Dê nome às threads com thread(name = ...) e peça as travas sempre na mesma ordem.
12. exercícios
exercício 1: soma em partes (guiado)
Some os números de 1 a 1 000 000 com quatro threads de um pool, em quatro partes de 250 000. Envie cada parte com submit e um Callable, some os get() e chame shutdown().
Resposta:
import java.util.concurrent.Callable
import java.util.concurrent.Executors
fun somaDoIntervalo(inicio: Long, fim: Long): Long {
var soma = 0L
for (n in inicio..fim) soma += n
return soma
}
fun main() {
val pool = Executors.newFixedThreadPool(4)
val partes = 4
val tamanho = 1_000_000L / partes
val futuros = (0 until partes).map { i ->
val inicio = i * tamanho + 1
val fim = (i + 1) * tamanho
pool.submit(Callable { somaDoIntervalo(inicio, fim) })
}
val total = futuros.sumOf { it.get() }
pool.shutdown()
println("soma de 1 a 1000000: $total")
}
-- JDK 21, execução 1 --
soma de 1 a 1000000: 500000500000
-- JDK 25, execução 1 --
soma de 1 a 1000000: 500000500000
O valor bate com n(n + 1) / 2 para n = 1 000 000.
exercício 2: a mesma ordem nos locks
Use o código da seção 9 e troque só o bloco da t2, para que ela pegue o lock A antes do B. Explique por que o ciclo some.
Resposta: só o bloco da t2 muda.
val t2 = thread(isDaemon = true) {
synchronized(trancaA) {
Thread.sleep(100)
synchronized(trancaB) { println("t2 terminou") }
}
}
-- JDK 21, execução 1 --
t1 terminou
t2 terminou
t1 viva: false, t2 viva: false
threads em deadlock: 0
-- JDK 25, execução 1 --
t1 terminou
t2 terminou
t1 viva: false, t2 viva: false
threads em deadlock: 0
Quem pega o lock A primeiro termina e solta os dois. A outra thread espera fora, sem segurar nada, e o ciclo não se forma.
13. glossário
- Processo: programa em execução, com memória própria.
- Thread: linha de execução dentro de um processo.
- Heap: memória compartilhada pelas threads, com objetos e campos estáticos.
- Pilha de thread: memória privada de cada thread, com as variáveis locais.
- Condição de corrida (race condition): erro em que o resultado depende da intercalação das threads.
- Exclusão mútua: garantia de que só uma thread executa um trecho crítico por vez.
- Monitor (lock intrínseco): trava que a JVM associa a cada objeto.
- Deadlock: ciclo de espera em que nenhuma thread avança.
- Pool de threads: conjunto de threads que pegam tarefas de uma fila.
- Callable e Future: tarefa que devolve valor, e o objeto que representa esse valor pendente.
- Thread daemon: thread que não impede a JVM de encerrar.
o que não foi confirmado ou medido
- Plataforma: Mac com Apple silicon, OpenJDK 21.0.8 (Temurin) e OpenJDK 25. Windows, Linux e IntelliJ não foram testados. Bytecode para JVM 21, com Gradle 9.7.0 e Kotlin 2.4.21.
- Valores: as saídas são de execuções específicas. A ordem saiu igual no JDK 25 nas cinco execuções, o que não garante nada.
- volatile: o resultado depende do JIT. O programa de teste não entrou na aula.
- Custo do synchronized: sem aquecimento separado nem repetições estatísticas. Use como ordem de grandeza.
- Memória por thread e limite do sistema: não medidos.
- Programas de medição: os códigos das medições das seções 5, 7, 8 e 10 (custo do
synchronized, teste dovolatile, pool semshutdown()e 10 000 threads) não estão nesta nota. Os números citados não são reproduzíveis só pelo texto. Modelo de memória e atomics ficam para outras aulas.
Fontes
Consultadas em 04/11/2026.
- Kotlin,
thread(): https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.concurrent/thread.html - Kotlin,
Synchronized: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.jvm/-synchronized/ - Kotlin,
Volatile: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.jvm/-volatile/ - Oracle, Intrinsic Locks and Synchronization: https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html
- Oracle,
Thread: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html - Oracle,
Executors: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Executors.html - Oracle,
ExecutorService: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ExecutorService.html - Oracle,
Future: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Future.html - Oracle,
Callable: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Callable.html - Oracle,
ThreadMXBean: https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/ThreadMXBean.html
Relacionados
- suspend e a máquina de estados
- Dispatchers e bloqueio
- Estado compartilhado: Mutex e atomics
- Virtual threads e testes com runTest
- Funções
- Nova ordem da trilha de Kotlin