CodeGym /Cursos /JAVA 25 SELF /Introdução ao paralelismo

Introdução ao paralelismo

JAVA 25 SELF
Nível 54 , Lição 0
Disponível

1. Multithreading vs paralelismo

Multithreading: muito, mas não necessariamente ao mesmo tempo

Multithreading é quando o seu programa tem várias threads de execução. Cada thread é como uma linha de ação independente: uma calcula algo, outra espera a entrada do usuário, a terceira salva dados em um arquivo. Em Java, você cria threads via a classe Thread, implementa a interface Runnable ou usa ferramentas de alto nível como ExecutorService (sobre elas — na próxima aula).

MAS! Multithreading não garante que suas tarefas realmente rodem ao mesmo tempo. Tudo depende de quantos núcleos seu processador tem. Se houver apenas um núcleo, as threads apenas “alternam” rapidamente entre si — tão rápido que parece que tudo acontece ao mesmo tempo. Na prática, o processador executa apenas uma thread em cada momento, e as demais aguardam a sua vez.

Paralelismo: quando as tarefas realmente ocorrem ao mesmo tempo

Paralelismo é quando seu código realmente é executado simultaneamente em vários núcleos do processador. Se você tem um computador moderno com 4, 8, 16 núcleos — você pode de fato acelerar o processamento de grandes tarefas, dividindo-as em partes independentes e distribuindo-as pelos núcleos.

Fazendo uma analogia, multithreading é quando você tem um único cozinheiro, que alterna rapidamente entre preparar borsch, fritar hambúrgueres e cortar salada. Paralelismo é quando você tem vários cozinheiros ao mesmo tempo, e cada um cuida do seu prato.

Qual é a diferença na prática?

Multithreading diz respeito à conveniência e à responsividade. Você usa várias threads para o programa não “travar”: uma thread espera a rede, outra renderiza a interface, a terceira calcula algo. Tudo funciona em paralelo na percepção, mas não necessariamente ao mesmo tempo.

Paralelismo é sobre velocidade. Aqui, de fato, vários núcleos do processador executam partes diferentes da tarefa simultaneamente, para obter o resultado mais rápido.

Em outras palavras: multithreading ajuda a organizar o trabalho, e o paralelismo — a acelerá-lo.

Importante:
Multithreading é necessário sempre que há tarefas que podem ser feitas de forma independente.
Paralelismo é necessário quando você quer acelerar os cálculos graças à distribuição real do trabalho entre os núcleos.

Exemplo: processamento de um array grande

Vamos supor que temos um array de 10 milhões de números e queremos calcular a soma de todos os elementos.

Sequencialmente:
Uma única thread percorre todo o array e calcula a soma. Simples e confiável, mas demorado.

Multithread (mas em um único núcleo):
Você divide o array em 4 partes, cria 4 threads, cada uma soma a sua parte. Mas se você tem apenas um núcleo, as threads vão apenas executar em turnos — não haverá aceleração, e o overhead de troca de contexto pode até deixar o programa mais lento.

Paralelo (em vários núcleos):
Você divide o array em 4 partes, inicia 4 threads, e cada thread realmente roda em seu próprio núcleo. A soma final é composta de 4 partes. Isso é de fato mais rápido — especialmente em grandes volumes de dados.

Por outro lado, implementar o processamento sequencial do array é muito simples; você já escreveu esse tipo de programa muitas vezes:

// Exemplo: processamento sequencial de um array
int[] arr = new int[10_000_000];
// ... preenchimento do array ...
long sum = 0;
for (int x : arr) {
    sum += x;
}
System.out.println(sum);

As versões multithread e paralela são um pouco mais complexas; vamos estudá-las nas próximas aulas com ferramentas modernas.

2. Para que serve o paralelismo

Os processadores modernos há muito deixaram de ser de um único núcleo. Mesmo o seu smartphone, muito provavelmente, tem pelo menos quatro, e desktops e servidores — oito, dezesseis, trinta e dois ou mais. Se o aplicativo consegue usar todos esses núcleos, ele pode funcionar muitas vezes mais rápido.

Antes, o desempenho dos processadores crescia com o aumento da frequência de clock — até meados dos anos 2000 isso realmente funcionou. Mas o aumento de frequência esbarrou em limitações físicas, e então começou uma nova era — sistemas multiprocessados e multinúcleo. Agora se destacam os programas que conseguem distribuir o trabalho de forma eficiente entre os núcleos.

Onde o paralelismo realmente acelera?

  • Processamento de grandes volumes de dados: análise de logs, estatística, agregação — tudo o que pode ser dividido em pedaços independentes.
  • Renderização, processamento de imagens e vídeo: cada pixel ou fragmento pode ser processado separadamente.
  • Cálculos científicos, modelagem: problemas matemáticos, simulações, treinamento de modelos.
  • Aplicações de servidor: atendimento simultâneo a muitos clientes.
  • Aplicações reativas: quando é preciso reagir rapidamente a muitos eventos, sem bloquear a thread principal.

Quando o paralelismo não ajuda?

  • Se a tarefa for pequena, o overhead de iniciar o paralelismo pode ser maior do que o ganho.
  • Se a tarefa não puder ser dividida em partes independentes (por exemplo, quando cada etapa depende da anterior).
  • Quando há muitos recursos compartilhados (por exemplo, o mesmo arquivo), e as threads começam a atrapalhar umas às outras.

3. Tarefas típicas para paralelismo

Vamos ver quais tarefas mais frequentemente são “distribuídas” entre os núcleos.

Cálculos massivos

  • Soma, busca de máximo/mínimo, cálculo de estatísticas em um array grande.
  • Exemplo: calcular o valor médio de temperatura de um milhão de sensores.

Processamento de coleções

  • Filtragem, ordenação, transformação de listas grandes (por exemplo, processamento de pedidos de uma loja online).
  • Exemplo: selecionar todos os pedidos acima de 10.000 rublos e ordená-los por data.

Renderização e processamento de gráficos

  • Aplicar um filtro a todos os pixels de uma imagem (por exemplo, deixá-la em preto e branco).
  • Cada pixel pode ser processado de forma independente — caso ideal para o paralelismo.

Análise de dados, big data

  • MapReduce, agregação, cálculo de estatísticas sobre volumes enormes de dados.
  • Exemplo: processar logs de um ano para buscar anomalias.

Exemplo: soma paralela
Suponha que temos um array de 1 milhão de números. É possível dividi-lo em 4 partes e calcular a soma de cada parte em uma thread separada, e depois somar os resultados.

4. Problemas e desafios do paralelismo

Dificuldade de depuração
Quando o código roda em várias threads, os bugs podem surgir apenas em casos raros, quando as threads “se cruzam” de um modo específico. Às vezes o erro aparece uma vez a cada 1000 execuções — e capturá-lo é muito difícil.

Condições de corrida (race condition)
Se várias threads alteram simultaneamente a mesma variável ou objeto — resultados incorretos são possíveis. Por exemplo, duas threads incrementam um contador ao mesmo tempo e o valor final fica menor do que o esperado.

Sincronização
Para evitar corridas, é preciso sincronizar o acesso a dados compartilhados — com a palavra-chave synchronized, locks, variáveis atômicas e outras ferramentas. Isso complica o código e pode levar a outros problemas (por exemplo, deadlock — bloqueio mútuo entre threads).

Balanceamento de carga
Se você dividiu a tarefa em 4 partes, e uma delas acabou muito mais pesada do que as outras — três threads já terminaram e ficam ociosas, enquanto a quarta ainda trabalha. No fim, não há aceleração.

Overhead
Criar threads, alternar entre elas, sincronizar — tudo isso consome tempo. Se a tarefa for pequena, o paralelismo apenas retardará a execução.

Tabela: comparação de abordagens

Abordagem Quando é rápido Quando desacelera Exemplo de uso
Sequencial (1 thread) Tarefas pequenas, lógica simples Grandes volumes de dados Processamento de 10 linhas
Multithreading (em 1 núcleo) Tarefas assíncronas (espera de IO) Tarefas CPU-bound (limitadas por CPU) em 1 núcleo Download simultâneo de arquivos
Paralelismo (muitos núcleos) Tarefas grandes e independentes Tarefas pequenas, acoplamento forte Processamento de um array grande

Visualização: como isso fica

// Processamento sequencial (1 thread)
[Tarefa 1][Tarefa 2][Tarefa 3][Tarefa 4]

// Multithreading em um único núcleo (alternância)
[Tarefa 1] [Tarefa 2] [Tarefa 3] [Tarefa 4]
(mas de fato apenas uma roda por vez, as outras esperam)

// Paralelismo em quatro núcleos
[Tarefa 1]    [Tarefa 2]    [Tarefa 3]    [Tarefa 4]
(todas executam simultaneamente)

5. Erros comuns ao tentar paralelizar

Erro nº 1: Paralelizar tudo sem critério. Muitos iniciantes pensam: “Quanto mais threads — mais rápido!”. Na prática, não é assim. Se as tarefas são poucas ou simples demais — o ganho não existe e, às vezes, o programa até fica mais lento.

Erro nº 2: Ignorar a sincronização. Se várias threads trabalham com os mesmos dados sem sincronização — você terá condições de corrida, lógica quebrada e bugs difíceis de capturar.

Erro nº 3: Paralelismo pelo paralelismo. Paralelismo não é um fim em si mesmo. Ele é necessário quando há tarefas reais que podem ser divididas de forma eficiente em partes independentes.

Erro nº 4: Desconsiderar as características da tarefa. Algumas tarefas não podem ser paralelizadas (por exemplo, quando o passo N+1 depende do resultado do passo N). Nesses casos, o paralelismo não traz vantagem.

Erro nº 5: Ignorar o overhead. Criar threads, alternar entre elas, agregar resultados — tudo isso consome tempo. Para tarefas pequenas, esse tempo pode ser maior do que o tempo do próprio trabalho.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION