Programação Paralela em Java Threads
Programação Paralela em Java Threads
Programação paralela em Java usando threads permite executar múltiplas tarefas ao mesmo tempo,
aumentando a eficiência do programa. Isso melhora o desempenho, especialmente em sistemas com
múltiplos processadores. Threads ajudam a dividir tarefas grandes em menores e executá-las
simultaneamente, o que é útil em aplicações que exigem alta performance, como jogos, processamento de
imagens e servidores web.
Objetivos
• Reconhecer o conceito de threads e sua importância para o processamento paralelo.
Introdução
Neste vídeo, vamos mostrar como a programação paralela em Java com threads pode aumentar a eficiência e
o desempenho dos programas. Aprenda a dividir grandes tarefas em partes menores e a executá-las ao
mesmo tempo, aproveitando vários processadores. Isso é ideal para aplicações de alta performance, como
jogos, processamento de imagens e servidores web.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
1. Threads e processamento paralelo
Com threads, podemos aproveitar múltiplos núcleos de processadores modernos, tornando os programas
mais rápidos. Em comparação, sem o uso de threads, o programa é executado sequencialmente, do início ao
fim, em um único núcleo do processador, demandando maior tempo de execução se comparado com o uso de
threads.
Neste vídeo, abordaremos o processamento paralelo com threads em Java, que permite a execução
simultânea de várias tarefas, maximizando o uso dos recursos do sistema.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Conceitos
Estamos tão acostumados com as facilidades da tecnologia que muitas vezes os mecanismos que atuam
ocultos são ignorados. Isso vale também para os desenvolvedores. No início da computação, contudo, não era
assim. Naquela época, bits de memória, ciclos de CPU e watts de energia gastos eram importantes. Aliás, não
se podia desenvolver um programa sem perfeito conhecimento do hardware. A linguagem de máquina e a
assembly (linguagem de montagem) eram os únicos recursos para programação, e operações de E/S (entrada/
saída) podiam demorar minutos, devendo ser evitadas a todo custo.
Comentário
Com o passar do tempo e o avanço tecnológico (surgimento de novas linguagens de programação,
barramentos mais velozes e CPUs mais rápidas), houve um aumento da complexidade e, com isso, os
compiladores passaram a agilizar muito o trabalho de otimização de código, gerando códigos de
máquina mais eficientes.
Na era da computação moderna, os processadores têm vários núcleos, que podem ser explorados para
aumentar a eficiência de execução de programas, especialmente aqueles grandes e complexos. Ou seja, ao
invés de um programa ser executado em apenas um núcleo do processador, ele pode explorar vários deles,
subdividindo as tarefas. Isso é possível graças a alguns fatores: sistemas operacionais multitarefa preemptiva,
linguagens que possibilitam a programação com várias threads (multithreading) e processadores com vários
núcleos, físicos e virtuais (hyperthreading).
Um sistema operacional multitarefa preemptiva é um tipo de sistema que permite a execução simultânea de
múltiplas tarefas (processos ou threads) e tem a capacidade de interromper (preemptar) a execução de uma
tarefa para iniciar ou continuar a execução de outra.
O termo thread ou processo leve consiste em uma sequência de instruções, uma linha de execução
dentro de um processo.
Para facilitar a compreensão do conceito de thread, vamos construir computadores teóricos com duas
configurações:
• CPU multinúcleo
Vamos supor que os dois computadores teóricos, um com CPU genérica de núcleo único e outro com CPU
multinúcleo, possuam sistemas operacionais multitarefa preempetivos, ou seja, capazes de controlar a
execução das tarefas nos processadores. Como exemplo, vamos considerar um programa fictício, com apenas
uma linha de código sendo executada nesses dois computadores. Esse exemplo simples fornecerá a base
para a compreensão de situações mais complexas envolvendo o conceito de threads.
Nossa primeira configuração era muito comum há pouco mais de uma década. Imagine que você está usando
o Word para fazer um trabalho e, ao mesmo tempo, está calculando a soma hash (soma utilizando algoritmo)
de um arquivo.
Na verdade, como já vimos, eles não estão. Essa é apenas uma ilusão criada pela preempção. Então, o que
acontece de fato? Vamos entender!
Toda vez que o tempo determinado pelo escalonador é atingido, essa troca de contexto ocorre, e as
operações são interrompidas mesmo que ainda não finalizadas. A sua execução é retomada quando o
processo volta ao contexto.
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
Escalonador de processos
Quanto mais softwares forem executados simultaneamente, mais a ilusão será perceptível, pois poderemos
perceber a lentidão na execução dos programas.
Vamos, então, considerar a segunda configuração (CPU multinúcleo): imagine uma família composta por
quatro pessoas que vai ao supermercado juntas no mesmo carro. Ao chegar no supermercado, cada um deles
fica responsável por comprar certos itens da lista de compras. Ou seja, eles trabalham de modo independente,
mas de forma colaborativa. Cada um deles passa em um caixa diferente do outro para realizar o pagamento.
Fica claro que essa abordagem reduz o tempo gasto para realizar a compra da família.
Voltando para o exemplo da computação, cada membro da família corresponde a uma thread e os caixas
representam os núcleos do processador, sendo um pipeline completo e independente dos demais (cada um
com seus próprios registradores, sem que a execução de um código em núcleo interfira na execução desse
mesmo código em outro núcleo). O conceito de pipeline em computação refere-se a uma técnica de
processamento na qual uma tarefa é dividida em várias etapas sequenciais. Cada etapa executa uma parte do
trabalho, e as diferentes etapas podem ser processadas em paralelo.
Claro que isso é verdade quando desconsideramos operações de I/O (entrada/saída) ou R/W (ler/escrever) em
memória. E, por simplicidade, essa será nossa abordagem. Podemos considerar, também por simplicidade,
que cada núcleo é idêntico ao nosso caso anterior.
Sempre que um software é executado, ele dispara um processo. Os valores de registrador, a pilha de
execução, os dados e a área de memória fazem parte do processo. Quando um processo é carregado em
memória para ser executado, uma área de memória é reservada e se torna exclusiva. Um processo pode criar
subprocessos, chamados também de processos filhos.
Vale lembrar que um programa é composto por diversas linhas de código escritas pelos desenvolvedores.
Essas linhas de código são executadas pelo processador, que realiza as ações correspondentes. Mas, diante
desse contexto, o que são threads e processos? As threads são linhas de execução de programa contidas nos
processos. Vamos diferenciar as threads de processos:
• Processos: programas em execução, cada um com seu próprio espaço de memória e recursos, isolados
uns dos outros.
Podemos subdividir trechos de um programa em várias threads e cada uma delas pode ser executada em um
núcleo diferente do mesmo processador. Como as threads são mais simples que os processos, sua criação,
finalização e trocas de contexto são mais rápidas, oferecendo a possibilidade de paralelismo com baixo custo
computacional quando comparadas aos processos. O fato de compartilharem a memória também facilita a
troca de dados, reduzindo a latência (atraso) envolvida nos mecanismos de comunicação interprocessos.
Atividade 1
Imagine ser convidado para assistir a um treinamento na empresa em que trabalha. A palestra aborda
programação multithreading, entre outros recursos. No final, o palestrante questiona qual seria uma vantagem
significativa do processamento paralelo com threads em Java, além de mostrar algumas opções descritas a
seguir. Qual é a resposta correta?
Neste vídeo, abordaremos como as threads em Java lidam com a execução de programas ao permitir
multitarefa simultânea. Veremos que é possível dividir programas em partes menores que executam em
paralelo, beneficiando-se dos múltiplos núcleos dos processadores modernos.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
A linguagem Java é uma linguagem de programação multithread, o que significa que Java suporta o conceito
de threads. Como vimos, uma thread pode ser preemptada da execução e isso é feito pelo sistema
operacional que emite comandos para o hardware. Por isso, nas primeiras versões da MVJ (máquina virtual
Java) o uso de threads era dependente da plataforma. Logo, se o programa usasse threads, ele perdia a
portabilidade oferecida pela MVJ. Com a evolução da tecnologia, a MVJ passou a abstrair essa funcionalidade,
de forma que tal limitação não existe atualmente.
Uma thread é uma maneira de implementar múltiplos caminhos de execução em uma aplicação.
Observe quantos programas estão em execução neste momento no seu computador. Talvez você esteja
utilizando um navegador, um editor de texto, antivírus, entre outros. Todos eles são executados no
processador. O seu computador deve ter um processador com vários núcleos e, assim, é importante tirar o
máximo proveito deles. Como isso é possível? É comum que vários programas sejam executados ao mesmo
tempo e o gerenciamento dessa execução fique por conta do sistema operacional (SO), que divide o tempo de
execução entre eles (execução em paralelo ou de forma preemptiva). Cada programa pode ter uma ou mais
partes que executam tarefas diferentes ao mesmo tempo, ou quase ao mesmo tempo.
Toda thread possui uma prioridade. A prioridade de uma thread é utilizada pelo escalonador da MVJ para
decidir o agendamento de que thread vai utilizar a CPU. Threads com maior prioridade têm preferência na
execução, porém é importante notar que ter preferência não é ter controle total. Suponha que uma aplicação
possua apenas duas threads, uma com prioridade máxima e a outra com prioridade mínima. Mesmo nessa
situação extrema, o escalonador deverá, em algum momento, preemptar a thread de maior prioridade e
permitir que a outra receba algum tempo de CPU. Na verdade, a forma como as threads e os processos são
escalonados depende da política do escalonador.
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
Escalonador de processos.
Isso é necessário para que haja algum paralelismo entre as threads. Do contrário, a execução se tornaria
serial, com a fila sendo estabelecida pela prioridade. Num caso extremo, em que novas threads de alta
prioridade continuassem sendo criadas, threads de baixa prioridade seriam adiadas indefinidamente.
Atenção
A prioridade de uma thread não garante um comportamento determinístico. Ter maior prioridade
significa apenas isso. O programador não sabe quando a thread será agendada.
Daemon
São threads de baixa prioridade, sempre executadas em segundo plano. Essas threads provêm
serviços para as threads de usuário (user threads), e sua existência depende delas, pois se todas as
threads de usuário finalizarem, a MVJ forçará o encerramento da daemon thread, mesmo que suas
tarefas não tenham sido concluídas. O Garbage Collector (GC) é um exemplo de daemon thread. Isso
esclarece por que não temos controle sobre quando o GC será executado e nem se o método finalize
será realizado.
User
São criadas pela aplicação e finalizadas por ela. A MVJ não força sua finalização e aguardará que as
threads completem suas tarefas. Esse tipo de thread executa em primeiro plano e possui prioridades
mais altas que as daemon threads. Isso não permite ao usuário ter certeza de quando sua thread
entrará em execução, por isso mecanismos adicionais precisam ser usados para garantir a
sincronicidade entre as threads. Veremos esses mecanismos mais à frente.
Atividade 2
Imagine que você esteja navegando em um fórum de computação e encontra uma pergunta de um usuário
sobre o que são threads em Java. Vários outros usuários postaram respostas, que são citadas a seguir. Qual
das seguintes afirmações melhor descreve threads em Java?
B Threads são processos separados que possuem seu próprio espaço de memória e recursos.
C Threads são unidades de execução dentro de um processo que compartilham o mesmo espaço de
memória.
E Threads são classes especiais em Java usadas para criar interfaces de usuário personalizadas.
Neste vídeo, serão abordados os estados de uma thread em Java, desde a criação até a terminação. As
threads transitam entre os estados New, Runnable, Blocked, Waiting, Timed Waiting e Terminated.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Quando a MVJ inicia, normalmente há apenas uma thread não daemon, que tipicamente chama o método main
das classes designadas. A MVJ continua a executar threads até que o método exit da classe Runtime é
chamado e o gerenciador de segurança permite a saída ou até que todas as threads que não são daemon
estejam mortas (ORACLE AMERICA INC., s.d.).
Toda thread possui um nome, mesmo que ele não seja especificado. Nesse caso, um nome será
automaticamente gerado. Veremos os detalhes de criação e uso de threads logo mais.
Uma thread pode existir em seis estados, conforme vemos na máquina de estados retratada na imagem a
seguir.
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
Escalonador de processos.
NEW
A thread está nesse estado quando é criada e ainda não está agendada para execução (SCHILDT,
2014).
RUNNABLE
A thread entra nesse estado quando sua execução é agendada (escalonamento) ou quando entra no
contexto de execução, isto é, passa a ser processada pela CPU (SCHILDT, 2014).
BLOCKED
A thread passa para este estado quando sua execução é suspensa enquanto aguarda uma trava
(lock). A thread sai desse estado quando obtém a trava (SCHILDT, 2014).
TIMED_WAITING
A thread entra nesse estado se for suspensa por um período, por exemplo, pela chamada do método
sleep () (dormindo), ou quando o timeout de wait () (esperando) ou join () (juntando) ocorre. A thread
sai desse estado quando o período de suspensão é transcorrido (SCHILDT, 2014).
WAITING
A thread entre nesse estado pela chamada aos métodos wait () ou join () sem timeout ou park ()
(estacionado) (SCHILDT, 2014).
TERMINATED
A thread chega a este estado, o último, quando encerra sua execução (SCHILDT, 2014).
É possível que em algumas literaturas você encontre essa máquina de estados com nomes diferentes.
Conceitualmente, a execução da thread pode envolver mais estados, e, sendo assim, você pode representar o
ciclo de vida de uma thread de outras formas. Mas além de isso não invalidar a máquina mostrada em nossa
figura, esses estados são os especificados pela enumeração State (ORACLE AMERICA INC., s.d.) da classe
Thread e retornados pelo método getState (). Isso significa que, na prática, esses são os estados com os
quais você irá operar numa implementação de thread em Java.
Comentário
Convém observar que, quando uma aplicação inicia, uma thread começa a ser executada. Essa thread é
usualmente conhecida como thread principal (main thread) e existirá sempre, mesmo que você não
tenha empregado threads no seu programa. Nesse caso, você terá um programa single thread, ou seja,
de thread única. A thread principal criará as demais threads, caso necessário, e deverá ser a última a
encerrar sua execução.
Quando uma thread cria outra, a mais recente é chamada de thread filha. Ao ser gerada, a thread receberá,
inicialmente, a mesma prioridade daquela que a criou. Além disso, uma thread será criada como daemon
apenas se a sua thread criadora for um daemon. Todavia, a thread pode ser transformada em daemon
posteriormente, pelo uso do método setDaemon().
Trata-se mais de oferecer alternativas em linha com os conceitos de orientação a objetos (OO). A extensão de
uma classe normalmente faz sentido se a subclasse vai acrescentar comportamentos ou modificar a sua
classe pai.
Implementação de “Runnable”
Como podemos perceber, a escolha de qual abordagem usar é mais conceitual do que prática.
A seguir, veremos três exemplos de códigos. O Código 1 e o Código 2 mostram a definição de threads com
ambas as abordagens, enquanto o Código 3 mostra o seu emprego.
java
java
…
// Extensão de Thread
ThreadSubclasse novaT = new ThreadSubclasse (200);
[Link] ();
// Implementação de Runnable
ThreadInterface novaT = new ThreadInterface (200);
new Thread ( novaT ).start ();
…
Atividade 3
Imagine que você esteja conversando com um colega de trabalho que está desenvolvendo um sistema em
Java. Ele tem algumas dúvidas sobre ciclo de vida de threads e faz a seguinte pergunta: qual estado de uma
thread em Java indica que ela está pronta para ser executada pelo escalonador, mas ainda não está em
execução?
A New
B Runnable
C Blocked
D Waiting
E Terminated
A alternativa B está correta.
Em síntese, no estado New, a thread foi criada, mas não está em execução. No estado Runnable, a thread
está pronta para ser executada pelo escalonador de threads do sistema operacional. No estado Blocked, a
execução da thread é suspensa. A thread entra no estado Waiting quando ocorre a chamada aos métodos
wait() ou join(). E a thread entra no estado Terminated quando encerra sua execução.
Neste vídeo, mostraremos como analisar os processos em execução no computador, como alterar a prioridade
deles, além de mostrar a quantidade de threads que cada processo tem.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Roteiro de prática
Considerando o sistema operacional Windows, mais especificamente o Windows 11, utilizado na elaboração
deste material, podemos observar os processos que estão em execução. Para isso, é necessário acessar o
gerenciador de tarefas.
Passo a passo:
1 - Utilize as teclas CTRL + ALT + DEL e, depois, selecione a opção gerenciador de tarefas. Na sequência,
selecione a opção Processos.
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
Exemplo de processos exibidos por meio do gerenciador de tarefas.
2 - Podemos ainda alterar a prioridade de processos. Para isso, ainda na tela do gerenciador de processos,
selecione a opção Detalhes.
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
3 - Depois, clique com o botão direito do mouse sobre o processo que você deseja alterar a prioridade. Neste
exemplo, o processo selecionado foi o Word. Escolha a opção Definir prioridade. Observamos que há várias
opções e, inicialmente, o Word tem prioridade Normal. Podemos alterá-la escolhendo alguma das opções
mostradas. Foi selecionada a opção Alta, e dessa forma, o Word terá maior prioridade de execução se
comparado a outros processos com prioridades inferiores.
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
5 - Depois, selecione a opção Threads, para que seja possível visualizar a quantidade de threads. O Word tem
45 threads no momento da geração da imagem. Veja!
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
Atividade 4
Imagine que você precise executar um software de outra empresa várias vezes, alterando algumas
configurações do algoritmo de aprendizado de máquina desse software, para mitigar os problemas de
classificação de fraudes em sistemas bancários. O que você pode fazer para acelerar a execução desse
software no seu computador?
A Uma vez que o software esteja pronto e em execução, não há o que fazer para melhorar seu
desempenho, ou seja, toda vez que o software iniciar, gastará o mesmo tempo de execução.
B O software precisa ser desenvolvido novamente, utilizando linguagem de programação funcional.
C Deve-se optar por executar o software utilizando computação em nuvem porque não é possível alterar o
tempo de execução de software em um computador.
D Você deve reduzir a quantidade de threads utilizadas no escalonador do sistema operacional porque,
quanto maior for a quantidade de threads, maior será o tempo de execução.
Paralelismo é a capacidade de executar várias operações ou tarefas ao mesmo tempo, aproveitando múltiplos
núcleos de processadores modernos. Utilizando threads, um programa pode dividir suas tarefas em partes
menores que rodam em paralelo, aumentando a eficiência e a velocidade de execução. Isso é essencial para
aplicações que requerem multitarefa, como processamento de dados em tempo real e interações de usuário.
Neste vídeo, analisaremos mais detalhes sobre threads e paralelismo. Veremos que as threads são unidades
de execução independentes dentro de um programa que permitem realizar múltiplas tarefas simultaneamente
em Java. Além disso, estudaremos o conceito de paralelismo, a capacidade de executar várias operações ao
mesmo tempo, aproveitando os múltiplos núcleos dos processadores modernos.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Conceitos
Imagine que desejamos realizar uma busca textual em um documento com milhares de páginas, com a
intenção de contar o número de vezes em que determinado padrão ocorre. Podemos fazer isso das seguintes
formas:
Método 1
Método 2
Outra abordagem possível é dividir o documento em várias partes e executar várias instâncias da
nossa aplicação simultaneamente. Apesar de conseguirmos reduzir o tempo de busca dessa forma,
ela exige a soma manual dos resultados, o que não se mostra uma solução elegante para um bom
programador.
Método 3
Podemos criar um certo número de threads e repartir as páginas do documento entre as threads,
deixando a própria aplicação consolidar o resultado. Essa solução, embora tecnicamente engenhosa,
é mais simples de descrever do que de fazer.
O método 3 parece ser o ideal para realizar nossa tarefa. Mas, ao paralelizar uma aplicação com o uso de
threads, duas questões importantes se colocam:
• Como realizar a comunicação entre as threads?
Cada thread pode estar sendo executada em um núcleo de CPU distinto, o que significa que elas
estão, de fato, correndo em paralelo. Suponha, agora, que duas encontrem o padrão buscado ao
mesmo tempo e decidam incrementar a variável de contagem também simultaneamente.
Em um nível mais baixo, o incremento é formado por diversas operações mais simples que envolvem a soma
de uma unidade, a leitura do valor acumulado e a escrita do novo valor em memória.
Lembre-se de que, no nosso exemplo, as duas threads estão fazendo tudo simultaneamente e que, sendo
assim, elas lerão o valor acumulado (digamos que seja X).
Ambas farão o incremento desse valor em uma unidade (X+1) e ambas tentarão escrever esse novo valor em
memória. Duas coisas podem ocorrer:
1. A colisão na escrita pode fazer com que uma operação de escrita seja descartada. Ou seja, quando
duas ou mais threads tentam escrever em uma variável ao mesmo tempo, pode acontecer que apenas
uma dessas operações seja realizada efetivamente.
2. Diferenças de microssegundos podem fazer com que as escritas ocorram com uma defasagem
infinitesimal. Isso significa que o intervalo de tempo entre duas operações de escrita pode ser tão curto
que o valor X+1 poderia ser escrito duas vezes.
Podemos resolver esse problema se conseguirmos coordenar as duas threads de maneira que, quando uma
inicie uma operação sobre a variável, a outra aguarde até que a operação esteja finalizada. Para fazermos
essa coordenação, será preciso que as threads troquem mensagens, contudo elas são entidades semi-
independentes rodando em núcleos distintos da CPU. Não se trata de dois objetos instanciados na mesma
aplicação. Aliás, precisaremos da MVJ para sermos capazes de enviar uma mensagem entre threads.
Felizmente esses e outros problemas consequentes do paralelismo de programação são bem conhecidos e há
técnicas para lidar com eles. A linguagem Java oferece diversos mecanismos para comunicação entre threads
e nas próximas seções vamos examinar dois deles:
• Semáforos
• Monitores
Atividade 1
Considere que você esteja participando de um processo seletivo para uma vaga de trabalho como
desenvolvedor e se deparou com a seguinte questão. Qual é o papel das threads e do paralelismo em Java?
Marque a resposta correta.
B Threads são partes de um programa que interagem com a interface do usuário. Paralelismo refere-se à
interação entre diferentes threads no mesmo programa.
Threads são unidades de execução dentro de um programa, permitindo a realização de múltiplas tarefas
Csimultaneamente. Paralelismo é a capacidade de executar várias tarefas ao mesmo tempo, aproveitando
múltiplos núcleos de processadores.
D Threads são pequenas funções que operam separadamente no código. Paralelismo envolve a
compactação de múltiplos threads em um único processo.
E Threads são usadas para depurar código em Java. Paralelismo refere-se à capacidade de isolar erros
de programação.
Ambos os conceitos são fundamentais para coordenar o acesso seguro a recursos compartilhados entre
threads, garantindo consistência e prevenindo erros de concorrência.
Neste vídeo, estudaremos semáforos e monitores, que facilitam a comunicação entre threads em Java.
Veremos como os semáforos atuam como contadores para controlar o acesso concorrente a recursos
compartilhados, enquanto os monitores garantem a execução segura de blocos críticos por uma única thread
por vez.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Semáforos
As técnicas para evitar os problemas já mencionados envolvem uso de travas, atomização de operações,
semáforos, monitores e outras. Essencialmente, o que buscamos é evitar as causas que levam aos problemas.
Por exemplo, ao usarmos uma trava sobre um recurso, evitamos o que é chamado de condição de corrida.
Vimos isso superficialmente no tópico anterior.
Comentário
Problemas inerentes a acessos compartilhados de recursos e paralelismo de processamento são muito
estudados em sistemas operacionais e sistemas distribuídos. O seu estudo detalhado excederia o nosso
propósito, mas vamos explorar essas questões dentro do contexto da programação Java.
1
Solicitação de acesso ao recurso
Quando uma thread deseja acesso a um recurso compartilhado, ela invoca o método de solicitação
de acesso. O número máximo de acessos ao recurso é dado pela variável de controle.
2
Controle de acessos
Quando uma solicitação de acesso é feita, se o número de acessos que já foi concedido for menor
do que o valor da variável de controle, o acesso é permitido e a variável é decrementada. Se o
acesso for negado, a thread é colocada em espera numa fila.
3
Liberação do recurso obtido
Quando uma thread termina de usar o recurso obtido, ela invoca o método que o libera e a variável
de controle é incrementada. Nesse momento, a próxima thread da fila é despertada para acessar o
recurso.
Desde a versão 5, Java oferece uma implementação de semáforo por meio da classe Semaphore (ORACLE
AMERICA INC., s.d.). Os métodos para acesso e liberação de recursos dessa classe são:
Acquire ()
Método que solicita acesso a um recurso ou uma região crítica, realizando o bloqueio até que uma
permissão de acesso esteja disponível ou a thread seja interrompida.
Release ()
Em Java, o número de acessos simultâneos permitidos é definido pelo construtor na instanciação do objeto.
Dica
O construtor também oferece uma versão sobrecarregada em que o segundo parâmetro define a justeza
(fair) do semáforo, ou seja, se o semáforo utilizará ou não uma fila (FIFO) para as threads em espera. Os
métodos acquire () e release () possuem uma versão sobrecarregada que permite a aquisição/liberação
de mais de uma permissão de acesso.
java
Caso o semáforo seja criado com o parâmetro fair falso, ele não utilizará uma FIFO.
Exemplo
Imagine que temos um semáforo que permite apenas um acesso à região crítica e que essa permissão
de acesso foi concedida a uma thread (thread 0). Em seguida, uma nova permissão é solicitada, mas
como não há acessos disponíveis, a thread (thread 1) é posta em espera. Quando a thread 0 liberar o
acesso, se uma terceira thread (thread 2) solicitar permissão de acesso antes de que a thread 1 seja
capaz de fazê-lo, ela obterá a permissão e bloqueará a thread 1 novamente.
O exemplo anterior também mostra um caso particular no qual o semáforo é utilizado como um mecanismo de
exclusão mútua, parecido com o mutex (mutual exclusion). Na prática, há diferença entre esses mecanismos:
Mutex
Semáforo
Faz a verificação para garantir que a
Não verifica se a liberação de acesso veio da
liberação veio da thread que a
mesma thread que a solicitou.
solicitou.
Como vimos, a checagem de propriedade diferencia ambos. Não obstante, um semáforo com o número
máximo de acessos igual a 1 também se comporta como um mecanismo capaz de realizar a exclusão mútua.
Vamos nos valer dessa diferença quanto à checagem para utilizar o semáforo para enviar sinais entre duas
threads. A ideia, nesse caso, é que a invocação de acquire () seja feita por uma thread (thread 0) e a
invocação de release (), por outra (thread 1). Vamos exemplificar:
3. A thread 1 invoca release (), o que incrementa a variável de controle do semáforo e desbloqueia a
thread 0.
Dessa forma, conseguimos enviar um sinal da thread 1 para a 0. Se utilizarmos um segundo semáforo com a
mesma configuração, mas invertendo quem faz a invocação dos métodos, teremos uma maneira de sinalizar
da thread 0 para a 1.
O exemplo a seguir facilitará o entendimento acerca do uso de semáforos na sinalização entre threads. Em
nosso exemplo, criaremos uma classe (PingPong) para disparar as outras threads.
Observe no código da nossa Thread Mãe (classe PingPong), a seguir, que as linhas 17 e 18 disparam as outras
threads. Os semáforos são criados nas linhas 11 e 12 com número de acesso máximo igual a zero. Isso é
necessário para permitir que ambas as threads (Ping e Pong) bloqueiem após o seu início.
java
//Métodos
public PingPong ( int tamanho_partida ) throws InterruptedException {
s1 = new Semaphore(0);
s2 = new Semaphore(0);
contador = new Controle ( tamanho_partida );
ping = new Ping ( s1 , s2 , contador );
pong = new Pong ( s1 , s2 , contador );
//juiz = new Juiz ( tamanho_partida / 2 );
new Thread ( ping ).start ();
new Thread ( pong ).start ();
[Link]();
}
}
Vamos analisar os códigos das outras threads, começando pela Thread A (classe Ping).
java
java
java
//Métodos
public static void main (String args[]) throws InterruptedException {
partida = new PingPong ( 8 );
}
}
Em nosso exemplo, uma classe (PingPong) é criada para disparar as outras threads, conforme vemos nas
linhas 17 e 18 do código da Thread Mãe. Os semáforos são criados com número de acesso máximo igual a
zero (linhas 11 e 12 da Thread Mãe), para permitir que ambas as threads (Ping e Pong) bloqueiem após o seu
início. O bloqueio ocorre quando a linha 19 das threads A e B são executadas. Ao executar a linha 19 do código
da Thread Mãe, a Thread A é desbloqueada e, a partir daí, há uma troca de sinalizações entre as threads até o
número máximo definido pela linha 7 da classe Principal.
plain-text
plain-text
Monitores
Vamos retornar ao problema hipotético apresentado no início do módulo. Nele, precisamos proceder ao
incremento de uma variável, garantindo que nenhuma outra thread opere sobre ela antes de terminarmos de
incrementá-la.
O que precisamos fazer, para evitar problemas, é ativar um controle imediatamente antes da leitura em
memória, dando início à proteção da operação. Após a última operação, o controle deve ser desativado.
Para casos como esse, a linguagem Java provê um mecanismo chamado de monitor. Um monitor é uma
implementação de sincronização de threads que permite:
No monitor, a exclusão mútua é feita por meio A cooperação implica que uma thread possa
de um mutex (lock) que garante o acesso abrir mão temporariamente do acesso ao
exclusivo à região monitorada. recurso, enquanto aguarda que alguma
condição ocorra. Para isso, um sistema de
sinalização entre as threads deve ser provido.
Ele recebe o nome de monitor porque se baseia no monitoramento de como as threads acessam os recursos.
Atenção
Classes, objetos ou regiões de códigos monitorados são ditos thread-safe, indicando que seu uso por
threads é seguro.
A linguagem Java implementa o conceito de monitor por meio da palavra reservada synchronized. Esse termo
é utilizado para marcar regiões críticas de código que, portanto, deverão ser monitoradas. Em Java, cada
objeto está associado a um monitor, que uma thread pode travar ou destravar. O uso de synchronized pode
ser aplicado a um método ou a uma região menor de código. Ambos os casos são mostrados no código a
seguir.
java
...
private Exemplo ex = new Exemplo (); //”ex” é uma referência para objetos do tipo
“Exemplo” (classe)
...
//Método sincronizado
public synchronized void decrementa ( ) {
conta--;
}
...
public void impressao () {
//Região de código sincronizada
synchronized (ex) {
[Link] (); //invoca o método “imprime ()” do objeto “ex” de maneira sincronizada
}
}
...
Em Java, todo objeto possui um wait-set associado que implementa o conceito de conjunto de threads. Essa
estrutura é utilizada para permitir a cooperação entre as threads, fornecendo os seguintes métodos:
wait ()
Adiciona a thread ao conjunto wait-set, liberando a trava que aquela thread possui e suspendendo
sua execução. A MVJ mantém uma estrutura de dados com as threads adormecidas que aguardam
acesso à região crítica do objeto.
notify ()
Acorda a próxima thread que está aguardando na fila e garante o acesso exclusivo à thread
despertada. Nesse momento a thread é removida da estrutura de espera.
notifyAll ()
Faz basicamente o mesmo que o método notify (), mas acordando e removendo todas as threads da
estrutura de espera. Entretanto, mesmo nesse caso apenas uma única thread obterá o travamento do
monitor, isto é, o acesso exclusivo à região crítica.
Você pode observar nos códigos da Thread A e Thread B de nosso exemplo anterior, que na linha 4
declaramos um objeto da classe Controle. Verificando a linha 18 fica claro que utilizamos esse objeto para
contar o número de execuções das threads. A cada execução da região crítica, o contador é decrementado
(linha 22). Essa situação é análoga ao problema que descrevemos no início e que enseja o uso de monitores.
E, de fato, como observamos no próximo código, os métodos decrementa () e getControle () são
sincronizados.
java
//Classe
public class Controle {
//Atributo
private int contador = 0;
//Métodos
public Controle(int contador) {
[Link] = contador;
}
Atividade 2
Imagine que você tenha acessado um fórum sobre desenvolvimento de software, mais especificamente sobre
comunicação entre threads. Alguém inseriu o seguinte questionamento: Como a comunicação entre threads é
facilitada em Java usando semáforos e monitores? Foram postadas várias respostas, indicadas nas opções a
seguir. Qual delas está correta?
A Semáforos garantem que apenas uma thread execute um bloco de código crítico por vez.
D Monitores garantem que várias threads acessem um recurso compartilhado ao mesmo tempo.
Objetos imutáveis
Objetos imutáveis são aqueles cujo estado não pode ser alterado após sua criação. Em programação
multithread em Java, objetos imutáveis oferecem segurança ao serem compartilhados entre threads. Como
não podem ser modificados, não há preocupações com condições de corrida ou sincronização. Cada thread
pode acessar simultaneamente um objeto imutável sem medo de alterações inesperadas. Essa característica
não apenas simplifica o desenvolvimento de software concorrente, mas também melhora a eficiência e a
robustez das aplicações ao minimizar os problemas de sincronização e acesso concorrente a dados.
Neste vídeo, estudaremos as características dos objetos imutáveis, que são cruciais na programação
multithread em Java. São objetos cujo estado não pode ser alterado após a criação e garantem segurança ao
serem compartilhados entre threads.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Um objeto é considerado imutável quando seu estado não pode ser modificado após sua criação. Objetos
podem ser construídos para ser imutáveis, mas a própria linguagem Java oferece classes de objetos com essa
característica. O tipo String é um caso de classe que define objetos imutáveis. Caso sejam necessários
objetos string mutáveis, Java disponibiliza duas classes, StringBuffer e StringBuilder, que permitem criar
objetos do tipo String mutáveis (SCHILDT, 2014).
O conceito de objeto imutável pode parecer uma restrição problemática, mas na verdade há vantagens. Uma
vez que já se sabe que o objeto não pode ser modificado, o código se torna mais seguro e o processo de
coleta de lixo mais simples. Já a restrição pode ser contornada ao criar um novo objeto do mesmo tipo que
contenha as alterações desejadas.
Se um objeto não pode ter seu estado alterado, não há risco de que ele se apresente num estado
inconsistente, ou seja, que tenha seu valor lido durante um procedimento que o modifica, por
exemplo. Acessos múltiplos de threads também não poderão corrompê-lo. Assim, objetos imutáveis
são thread-safe.
Em linhas gerais, se você deseja criar um objeto imutável, métodos que alteram o estado do objeto (set) não
devem ser providos. Também se deve evitar que alterações no estado sejam feitas de outras maneiras. Logo,
todos os campos devem ser declarados privados (private) e finais (final). A própria classe deve ser declarada
final ou ter seu construtor declarado privado.
Atenção
É preciso cuidado especial caso algum atributo faça referência a um objeto mutável. Essa situação exige
que nenhuma forma de modificação desse objeto seja permitida.
Podemos ver um exemplo de classe que define objetos imutáveis no código a seguir.
java
//Classe
public final class Aluno {
//Atributos
private final String nome;
private final long CPF;
private final int matricula;
//Métodos
protected Aluno ( String nome , long CPF , int matricula ) {
[Link] = nome;
[Link] = CPF;
[Link] = matricula;
}
Após observar os dois últimos exemplos, fica claro que a classe Controle não é imutável, razão pela qual
necessita do modificador synchronized. No entanto, o método usado para compartilhar o objeto contador
(linhas 6 e 13 do código Thread Mãe) é o mesmo. Cada thread a acessar o objeto imutável criado deve possuir
uma variável de referência do tipo da classe desse objeto. Uma vez criado o objeto, as threads precisam
receber sua referência e, a partir de então, terão acesso ao objeto.
Apesar de contador não ser um objeto imutável, ele exemplifica esse mecanismo de compartilhamento de
objetos entre threads. Na linha 13 da Thread Mãe ele é criado, e nas linhas 14 e 15 a referência para o objeto
criado é passada para as threads Ping e Pong.
Atividade 3
Durante uma palestra sobre programação multithreading, o palestrante perguntou ao público por que os
objetos imutáveis são recomendados em Java para esse tipo de programação. Vários participantes deram
suas respostas, listadas nas opções a seguir. Qual delas é a correta?
B Porque objetos imutáveis podem ser modificados por várias threads simultaneamente sem problemas.
C Porque objetos imutáveis garantem que o estado do objeto não será alterado após sua criação,
simplificando a sincronização entre threads.
D Porque objetos imutáveis são mais rápidos em comparação com objetos mutáveis.
Porque objetos imutáveis não precisam ser inicializados antes de serem compartilhados entre
E
threads.
A aplicação incorreta ou a falta de compreensão desses mecanismos pode causar condições de corrida,
levando a resultados não determinísticos e inesperados, como inconsistências nos dados, deadlocks ou falhas
na lógica do programa. Também há o deadlock (bloqueio mútuo) e inconsistências nos dados compartilhados
entre threads, resultando em comportamentos imprevisíveis e erros difíceis de diagnosticar. Portanto,
entender e aplicar corretamente semáforos e monitores é muito importante para desenvolver aplicações
robustas e eficientes em Java, garantindo a integridade dos dados e evitando problemas de concorrência que
poderiam comprometer a estabilidade e o desempenho do sistema em ambientes de execução simultânea de
tarefas.
Neste vídeo, estudaremos como semáforos e monitores são essenciais para garantir a estabilidade e robustez
das aplicações em Java, evitando problemas complexos, como condições de corrida e deadlock.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Roteiro de prática
Imagine uma classe Contador que possua um método incrementar() responsável por incrementar um contador
compartilhado entre múltiplas threads. A implementação inicial pode ser a seguinte:
java
Aparentemente, esse código pode parecer correto. No entanto, se duas ou mais threads acessarem o método
incrementar() simultaneamente, podemos enfrentar uma condição de corrida devido à natureza não atômica
da operação de incremento, ou seja, envolve três operações: leitura, incremento e gravação do novo valor do
incremento.
Para corrigir a condição de corrida no exemplo do contador compartilhado entre threads, podemos
implementar um semáforo em Java para sincronizar o acesso ao método incrementar(). Confira um exemplo
de como isso pode ser feito usando Semaphore da biblioteca padrão [Link]:
java
package multithreading;
import [Link];
Explicação do código:
• Método incrementar(): utiliza [Link]() para adquirir uma permissão antes de incrementar o
contador. Isso garante que apenas uma thread por vez possa executar o bloco crítico dentro do
método. Após a conclusão do incremento, [Link]() é chamado para liberar a permissão do
semáforo, permitindo que outras threads possam adquiri-la.
• getContador(): retorna o valor atual do contador de forma segura, já que a sincronização do acesso ao
contador é feita através do semáforo.
Com essa implementação, garantimos que qualquer operação de incremento no contador seja realizada de
forma exclusiva por uma thread de cada vez, eliminando a condição de corrida e mantendo a consistência dos
dados compartilhados entre threads.
Agora, vamos implementar a classe TesteContador para podermos demonstrar a execução da classe
Contador que contém um semáforo, confira:
java
package multithreading;
import [Link];
import [Link];
[Link]();
while(![Link]) {
// Espera até que todas as threads terminem
}
Para corrigir a condição de corrida no exemplo do contador utilizando um monitor em Java, podemos usar o
bloco synchronized para garantir que apenas uma thread execute o método incrementar() por vez. Aqui está
como podemos implementar isso:
java
Explicação do código:
• Método incrementar(): é declarado como synchronized, o que significa que apenas uma thread por vez
pode executar esse método para uma instância específica de Contador. Isso garante que não haverá
condições de corrida durante a operação de incremento do contador.
• Método getContador(): retorna o valor atual do contador de forma segura. Não há necessidade de
sincronização nesse método, pois ele apenas lê o valor do contador e não altera o estado do objeto.
Com essa implementação usando um monitor (synchronized), garantimos que o acesso concorrente ao
método incrementar() seja sincronizado, evitando problemas como condições de corrida e mantendo a
consistência dos dados compartilhados entre as threads.
Outro problema típico é o deadlock, que ocorre quando duas ou mais threads ficam bloqueadas
indefinidamente, aguardando que recursos exclusivos sejam liberados pela outra. Isso cria uma situação de
impasse em que nenhuma thread pode progredir, comprometendo a execução correta do programa.
Atividade 4
Imagine que você faz parte da equipe de desenvolvimento de um sistema que está apresentando problemas.
Depois de algum tempo, um colega de trabalho observou que faltava inserir a linha de código
[Link](), conforme mostrado no trecho de código a seguir, sendo que semaforo é um recurso do
tipo Semaphore. Ele fez a correção e o sistema passou a funcionar sem problemas. Qual é a explicação para
isso acontecer? Marque a resposta correta.
java
//linhas de código
[Link]();
B Se o sistema apresentou problemas, certamente não é por causa da linha [Link](), que
simplesmente possibilita alterar a prioridade de threads em Java.
A linha de código [Link]() possibilita acessar o atributo semaforo e, sem o método acquire(),
C
esse atributo não tem funcionalidade.
D O problema ocorreu porque a linha de código [Link]() é utilizada para transformar o objeto
semaforo em objeto imutável.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
O mundo da programação paralela é vasto, e mesmo as threads vão muito além do que este conteúdo pode
abarcar. Porém, desde que você tenha compreendido a essência, explorar todos os recursos que Java oferece
para programação paralela será questão de tempo e prática. Para auxiliá-lo a sedimentar os conhecimentos
adquiridos até o momento, vamos apresentar um exemplo que busca ilustrar os principais pontos que
abordamos, inicialmente fazendo uma introdução sucinta da classe Thread e seus métodos. Em seguida,
apresentaremos um caso prático e terminaremos com algumas considerações gerais pertinentes.
Como podemos concluir, os campos guardam as prioridades máxima, mínima e default da thread
respectivamente. A seguir, vamos conhecer alguns métodos relevantes:
1
getPriority () e setPriority (int pri)
O método getPriority () devolve a prioridade da thread, enquanto setPriority (int pri) é utilizado para
alterar a prioridade da thread. Quando uma nova thread é criada, ela herda a prioridade da thread
que a criou. Isso pode ser alterado posteriormente pelo método setPriority (int pri), que recebe como
parâmetro um valor inteiro correspondente à nova prioridade a ser atribuída. Observe, contudo, que
esse valor deve estar entre os limites mínimo e máximo, definidos respetivamente por MIN_PRIORITY
e MAX_PRIORITY.
2getState ()
Outro método relevante é o getState (). Esse método retorna o estado no qual a thread se encontra,
com vimos na figura da máquina de estados da thread (os estados possíveis da thread são: NEW,
RUNNABLE, BLOCKED, TIMED_WAITING, WAITING ou TERMINATED) no início deste estudo e está
descrito na documentação da classe State (ORACLE AMERICA INC., s.d.). Embora esse método possa
ser usado para monitorar a thread, ele não serve para garantir a sincronização. Isso acontece porque
o estado da thread pode se alterar entre o momento em que a leitura foi realizada e o recebimento
dessa informação pelo solicitante, de maneira que a informação se torna obsoleta.
3
getId () e getName ()
Os métodos getId () e getName () são utilizados para retornar o identificador e o nome da thread. O
identificador é um número do tipo long gerado automaticamente no momento da criação da thread, e
permanece inalterado até o fim de sua vida. Apesar de o identificador ser único, ele pode ser
reutilizado após a thread finalizar.
4
setName ()
O nome da thread pode ser definido em sua criação, por meio do construtor da classe, ou
posteriormente, pelo método setName (). O nome da thread é do tipo String e não precisa ser único.
Na verdade, o sistema se vale do identificador e não do nome para controlar as threads. Da mesma
forma, o nome da thread pode ser alterado durante seu ciclo de vida.
5
currentThread ()
Caso seja necessário obter uma referência para a thread corrente, ela pode ser obtida com o método
currentThread (), que retorna uma referência para um objeto Thread. A referência para o próprio
objeto (this) não permite ao programador acessar a thread específica que está em execução.
6
join ()
Para situações em que o programador precise fazer com que uma thread aguarde outra finalizar para
prosseguir, a classe Thread possui o método join (), que ocorre em três versões, sendo
sobrecarregado da seguinte forma: join (), join (long millis) e join (long millis, int nanos). Suponha que
uma Thread A precisa aguardar a Thread B finalizar antes de prosseguir seu processamento. A
invocação de [Link] () em A fará com que A espere (wait) indefinidamente até que B finalize. Repare
que, se B morrer, A permanecerá eternamente aguardando por B.
Uma maneira de evitar que A se torne uma espécie de “zumbi” é especificar um tempo limite de
espera (timeout), após o qual ela continuará seu processamento, independentemente de B ter
finalizado. A versão join (long millis) permite definir o tempo de espera em milissegundos, e a outra,
em milissegundos e nanossegundos. Nas duas situações, se os parâmetros forem todos zero, o
efeito será o mesmo de join ().
7run ()
É o método principal da classe Thread. Esse método modela o comportamento que é realizado pela
thread quando ela é executada e, portanto, é o que dá sentido ao emprego da thread. Os exemplos
mostrados nos códigos das threads A e B ressaltam esse método sendo definido numa classe que
implementa uma interface Runnable. Mas a situação é a mesma para o caso em que se estende a
classe Thread.
8
setDaemon ()
O método setDaemon () é utilizado para tornar uma thread, um daemon ou uma thread de usuário.
Para isso, ele recebe um parâmetro do tipo boolean. A invocação de setDaemon ( true ) marca a
thread como daemon. Se o parâmetro for “false”, a thread é marcada como uma thread de usuário.
Essa marcação deve ser feita, contudo, antes de a thread ser iniciada (e após ter sido criada). O tipo
de thread pode ser verificado pela invocação de isDaemon (), que retorna “true” se a thread for do
tipo daemon.
9
sleep (long millis)
É possível suspender temporariamente a execução de uma thread utilizando o método sleep (long
millis), o qual faz com que a thread seja suspensa pelo período de tempo em milissegundos
equivalente a millis. A versão sobrecarregada sleep (long millis, int nanos) define um período em
milissegundos e nanossegundos. Porém, questões de resolução de temporização podem afetar o
tempo que a thread permanecerá suspensa de fato. Isso depende, por exemplo, da granularidade
dos temporizadores e da política do escalonador.
10
start () e stop ()
Talvez o método start () seja o mais relevante depois de run (). Esse método inicia a execução da
thread, que passa a executar run (). O método start () deve ser invocado após a criação da thread e é
ilegal invocá-lo novamente em uma thread em execução. Há um método que para a execução da
thread (stop ()), mas, conforme a documentação, esse método está depreciado desde a versão 1.2. O
seu uso é inseguro devido a problemas com monitores e travas e, em consequência disso, deve ser
evitado. Uma boa discussão sobre o uso de stop () pode ser encontrada nas referências deste
material.
11
yield ()
O último método que abordaremos é o yield (). Esse método informa ao escalonador do sistema que a
thread corrente deseja ceder seu tempo de processamento. Ao ceder tempo de processamento,
busca-se otimizar o uso da CPU, melhorando a performance. Contudo, cabem algumas observações:
primeiramente, quem controla o agendamento de threads e processos é o escalonador do sistema,
que pode perfeitamente ignorar yield (). Além disso, é preciso bom conhecimento da dinâmica dos
objetos da aplicação para se extrair algum ganho pelo seu uso. Tudo isso torna o emprego de yield ()
questionável.
Aqui não abordamos todos os métodos da classe Thread. Procuramos apenas examinar aqueles necessários
para implementações básicas usando threads e que lhe permitirão explorar a programação paralela.
A API Java oferece outras classes úteis e importantes, a Semaphore e CountDownLatch, cuja familiaridade
virá do uso. Aliás, conforme você melhore suas habilidades em programação com threads, descobrirá outros
recursos que a API Java oferece. Por enquanto, para consolidar o aprendizado, vamos apresentar um exemplo
que emprega diversos conhecimentos vistos anteriormente.
Atividade 1
Imagine que você esteja conversando com estagiários da empresa onde trabalha como desenvolvedor de
sistemas. Em um momento, você perguntou a opinião deles sobre a importância dos conceitos de threads,
especificamente em Java. As respostas estão listadas a seguir. Qual delas você considera mais adequada?
Neste vídeo, estudaremos a importância do conhecimento prático na implementação de threads em Java para
o desenvolvimento de aplicações eficientes e responsivas.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Roteiro de prática
Vamos analisar um exemplo simples e puramente didático, considerando uma empresa que trabalha com
encomendas. Veja o diagrama de classes!
Conteúdo interativo
Acesse a versão digital para ver mais detalhes da imagem
abaixo.
Diagrama de classes.
A classe Principal é a que possui o método main e se limita a disparar a execução da aplicação. Ela pode ser
vista no código a seguir.
java
//Métodos
public static void main ( String args [ ] ) throws InterruptedException {
// Empresa (número de fitas, empregados disponíveis, número máximo de equipes,
produtos a serem empacotados)
ACME = new Empresa ( 20 , 25 , 4 , 200 );
}
}
Essa classe, que é a primeira thread a ser criada quando um programa é executado, instancia a classe
Empresa. A instância ACME possui 20 fitas, 25 empregados e pode usar até 4 equipes para empacotar 200
produtos. Cada equipe formada corresponderá a uma thread, e cada empregado alocado também. Assim, a
thread de uma equipe criará outras threads correspondentes aos seus membros. São os objetos Empacotador,
que correspondem ao membro da equipe, que realizarão o empacotamento.
A classe Empresa realiza a montagem das equipes, distribuindo os funcionários, e inicia as threads
correspondentes às equipes formadas. Os métodos comporEquipes e criarEquipes trabalham juntos para criar
as equipes e definir quantos membros cada uma possuirá. Porém, o trecho que mais nos interessa nessa
classe é o compreendido entre as linhas 33 e 43. Veja o código a seguir, que mostra a classe Empresa.
java
package [Link];
import [Link];
import [Link];
import [Link];
/**
*
* @author Prof Marlos M Corrêa
*/
public class Empresa {
//Atributos
private final Semaphore pool_fita; //Controla o acesso ao recurso crítico (fitas).
private final PoolProdutos pool_produtos; //Produtos a serem empacotados.
private final ArrayList < Equipe > turno; //Conjunto de equipes de empacotadores.
Cada equipe possui 2 ou mais empacotadores e corresponde a uma thread.
private final int max_prod_empacotar; //Número máximo de produtos que serão
empacotados.
private final int pool_empacotadores; //Número de empacotadores disponíveis para
formar as equipes. Cada empacotador também corresponde a uma thread.
private final int nr_max_equipes; //Número máximo de equipes que podem ser
formadas.
private int prod_empacotados; //Número de produtos empacotados.
//Métodos
public Empresa(int nr_fitas, int pool_empacotadores, int nr_max_equipes, int
max_prod_empacotar) throws InterruptedException {
if((nr_fitas < 1) || (pool_empacotadores < 2) || (nr_max_equipes < 1) ||
(max_prod_empacotar < 1))
throw new IllegalArgumentException("Argumentos ilegais utilizados
no construtor de Empresa.");
else {
this.pool_fita = new Semaphore(nr_fitas);
this.pool_empacotadores = pool_empacotadores;
this.nr_max_equipes = nr_max_equipes;
this.max_prod_empacotar = max_prod_empacotar;
this.pool_produtos = new PoolProdutos(max_prod_empacotar);
[Link] = new ArrayList < Equipe > ();
this.prod_empacotados = 0;
criarEquipes(nr_fitas); //Monta as equipes alocando os
empacotadores e armazenando as equipes em "turno".
[Link]((eqp) - > [Link]()); //Inicia todas as threads.
for(Equipe eqp: turno) //Faz o join com todas as threads de
equipe.
try {
[Link](); //A thread principal deve aguardar o
fim de todas as threads Equipe para poder contabilizar os empacotamentos.
} catch (InterruptedException e) {
[Link]();
}
for(Equipe eqp: turno)
prod_empacotados = prod_empacotados +
[Link](); //Contabiliza o total de empacotamentos.
[Link]("TOTAL DE EMPACOTAMENTOS: " +
prod_empacotados);
}
}
private void criarEquipes(int nr_fitas) {
Equipe eqp;
int nr_emp_eqp;
int empacotadores_disponiveis = pool_empacotadores;
int i = 1;
do { //Cria as equipes com um número aleatório de integrantes e as
adiciona em "turno".
nr_emp_eqp = comporEquipe(empacotadores_disponiveis);
if(nr_emp_eqp > nr_fitas)
nr_emp_eqp = nr_fitas; //Do contrário, teríamos mais
Vejamos, então, como a classe Equipe funciona. Para isso, veja o próximo código.
java
/**
*
* @author Prof Marlos M Corrêa
*/
public class Equipe extends Thread {
//Atributos
private final int nr_integrantes;
private CountDownLatch latch;
private final Semaphore pool_fita;
private final PoolProdutos pool_produtos;
private final ArrayList < Empacotador > empacotadores;
private final ContadorSinc prod_empacotados_eqp;
//Métodos
public Equipe(String nome, int nr_integrantes, Semaphore pool_fita, PoolProdutos
pool_produtos) {
[Link](nome);
this.nr_integrantes = nr_integrantes;
this.pool_fita = pool_fita;
this.pool_produtos = pool_produtos;
[Link] = new ArrayList < Empacotador > ();
prod_empacotados_eqp = new ContadorSinc(0);
prepararEmpacotadores();
}
/**
* Realiza atomicamente as operações de decremento do latch, incremento do número
de pacotes
* que a equipe empacotou e libera (uma) trava sobre o semáforo "pool_fita".
*/
public synchronized void liberarFita() {
[Link]();
prod_empacotados_eqp.incrementar();
pool_fita.release();
}
/**
* Realiza atomicamente as operações de decremento do latch e libera "nr_travas"
travas sobre o semáforo "pool_fita".
*/
public synchronized void liberarFitasDesnecessarias(int nr_travas_liberadas) {
pool_fita.release(nr_travas_liberadas);
while(nr_travas_liberadas > 0) {
[Link]();
nr_travas_liberadas--;
}
}
public synchronized int getEmpacotamentos() {
return prod_empacotados_eqp.getContador();
}
public synchronized int getNrIntegrantes() {
return nr_integrantes;
}
private void prepararEmpacotadores() {
for(int i = 1; i <= nr_integrantes; i++) {
Empacotador emp = new Empacotador(i, this);
[Link](emp);
}
}
/**
* Para cada empregador, se houver pacote disponível para empacotar, decrementa
pool_pacotes, dispara uma
* thread (Empacotador) para realizar o trabalho de empacotamento. Do contrário,
A classe Empacotador é mostrada no próximo código.
java
package [Link];
//Importações
import [Link];
import [Link];
/**
*
* @author Prof Marlos M Corrêa
*/
public class Empacotador implements Runnable {
//Atributos
private final Equipe equipe;
private final ContadorSinc empacotamentos;
private final String nome;
private String lista_threads_id;
//Métodos
public Empacotador(int nr_empacotador, Equipe equipe) {
[Link] = equipe;
this.lista_threads_id = new String();
[Link] = "Emp[" + nr_empacotador + "]@" + [Link]();
[Link]().setName(nome);
empacotamentos = new ContadorSinc(0);
}
public void listarIdThreads() {
[Link](" |----- Lista de threads executadas por " + nome +
" : " + lista_threads_id);
}
public void listarEmpacotamentos() {
[Link](" |----- Empacotamentos feitos por " + nome + " : " +
[Link]());
}
@Override
public void run() {
try {
synchronized(lista_threads_id) {
lista_threads_id = lista_threads_id + "[" +
[Link]().getId() + "]"; //Constrói na instância de Empacotador uma lista
com todas as threads que foram criadas.
}
[Link](nome + " empacotando (" +
[Link]() + ")");
[Link]((int)([Link]() * 899 + 100)); //Coloca a thread
para dormir por um período aleatório entre 100 e 999 milissegundos.
[Link](nome + " concluiu (" +
[Link]() + ")");
[Link](); //Incrementa o contador de
empacotamentos da instância de Empacotador.
[Link]();
} catch (InterruptedException e) {
[Link]();
}
}
}
A ocorrência de mais de uma chamada concorrente pode levar a uma condição de corrida. Para impedir isso,
usamos synchronized, garantindo que somente uma execução do método ocorra ao mesmo tempo.
java
package [Link];
/**
*
* @author Prof Marlos M Corrêa
*/
public class ContadorSinc {
//Atributo
private int contador;
private final int inicio;
//Métodos
public ContadorSinc ( int inicio ) {
[Link] = inicio;
[Link] = inicio;
}
public synchronized void decrementar ( ) {
[Link]--;
}
public synchronized void decrementar ( int n ) {
[Link] = [Link] - n;
}
public synchronized void incrementar ( ) {
[Link]++;
}
public synchronized void incrementar ( int n ) {
[Link] = [Link] + n;
}
public synchronized void resetContador ( ) {
[Link] = [Link];
}
public synchronized void zerarContador ( ) {
[Link] = 0;
}
public synchronized int getContador ( ) {
return [Link];
}
}
Nossa última classe é a PoolProdutos (próximo código). Ela também é um contador que deve ser
compartilhado por mais de uma thread, mas precisamos modelar um comportamento adicional, representado
pelo método retirarProdutos na linha 15. Para isso, estendemos a classe ContadorSicn, adaptando-a para essa
nova funcionalidade. Note que continuamos usando o synchronized, pelas mesmas razões de antes.
java
package [Link];
/**
*
* @author Prof Marlos M Corrêa
*/
public final class PoolProdutos extends ContadorSinc {
//Métodos
public PoolProdutos(int qtdade_produtos) {
super(qtdade_produtos);
if(qtdade_produtos < 1)
throw new IllegalArgumentException("Argumentos ilegais utilizados
no construtor de PoolProdutos.");
}
public synchronized int retirarProdutos(int nr_produtos) {
int aux = getContador();
if((aux - nr_produtos) >= 0) { //Há produtos disponíveis suficientes para
atender à retirada.
decrementar(nr_produtos);
return nr_produtos;
} else { //Os produtos são insuficientes ou inexistentes.
zerarContador();
return aux;
}
}
}
Atividade 2
Suponha que você esteja desenvolvendo um sistema que usa programação multithreading em Java, e uma
classe específica está apresentando problemas. O trecho de código a seguir se refere a essa classe.
java
A Não há erros no trecho de código mostrado; o problema está em outro trecho de código.
Na linha 1, não se trata da definição de uma classe, mas de uma interface. Portanto, a linha 1 deve ser
B
substituída por: public interface ContadorSinc {.
C A linha 9 está errada porque deve-se utilizar Semaphore e não monitor como indicado no trecho de
código. Portanto, a linha 9 deve ser substituída por: public Semaphore void decrementar ( ) {.
DA linha 9 está errada porque deve-se utilizar synchronized e não monitor como indicado no trecho de
código. Portanto, a linha 9 deve ser substituída por: public synchronized void decrementar ( ) {.
E Não se pode implementar threads que realizem operação de decremento, conforme indicado no
código.
Este vídeo explora a importância do conhecimento teórico em programação paralela e uso de threads em Java
para o desenvolvimento e aplicações multitarefa robustas e eficientes.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
A programação paralela é desafiadora. É fácil pensar de maneira sequencial, com todas as instruções
ocorrendo de forma encadeada ao longo de uma única linha de execução, mas quando o programa envolve
múltiplas linhas que se entrecruzam, a situação suscita problemas inexistentes no caso de uma única linha.
A chamada condição de corrida frequentemente se faz presente, exigindo do programador uma atenção
especial. Vimos os mecanismos que Java oferece para permitir a sincronização de threads, mas esses
mecanismos precisam ser apropriadamente empregados. Dependendo do tamanho do programa e do número
de threads, controlar essa dinâmica mentalmente é desejar o erro.
Erros em programação paralela são mais difíceis de localizar, pela própria forma como o sistema
funciona.
Há algumas práticas simples que podem auxiliar o programador a evitar os erros, como:
Escolha da IDE
Atualmente, as IDE evoluíram bastante. O Apache Netbeans, por exemplo, permite, durante a
depuração, mudar a linha de execução que se está examinando. Porém, como os problemas
geralmente advêm da interação entre as linhas, a depuração pode ser difícil e demorada mesmo com
essa facilidade da IDE.
Uso da UML
Um bom profissional de programação é ligado a metodologias. E uma boa prática, nesse caso, é a
elaboração de diagramas dinâmicos do sistema, como o diagrama de sequência e o diagrama de
objetos da UML (em inglês, Unified Modeling Language; em português, Linguagem Unificada de
Modelagem), por exemplo. Esses são mecanismos formais que permitem compreender a interação
entre os componentes do sistema.
Há sutilezas na programação que muitas vezes passam despercebidas e podem levar o software a se
comportar de forma diferente da esperada, já que a linguagem Java oculta os mecanismos de
apontamento de memória. Se por um lado isso facilita a programação, por outro exige atenção do
programador quando estiver trabalhando com tipos não primitivos. Por exemplo, uma variável do tipo
int é passada por cópia, mas uma variável do tipo de uma classe definida pelo usuário é passada por
referência. Isso tem implicações importantes quando estamos construindo um tipo de dado imutável.
java
//Métodos
protected Imutavel ( ) {
[Link] = new Contador (0);
}
}
Queremos construir uma classe que nos fornecerá um objeto imutável. Por sua simplicidade, e já que a
tornamos final, assim como seu único atributo, esse deveria ser o caso. Mas examinemos melhor a linha 3.
Essa linha diz que conta é uma referência imutável. Isso quer dizer que, uma vez instanciada (linha 7), ela não
poderá se referenciar a outro objeto, mas nada impede que o objeto por ela apontado se modifique, o que
pode ocorrer se a referência vazar ou se o próprio objeto realizar interações que o levem a tal.
Atenção
Lembre-se: quando se trata de tipos não primitivos, a variável é uma referência de um tipo, e não o tipo
em si.
Como se não bastassem todas essas questões, temos o escalonador do sistema, que pode fazer o software
se comportar diferentemente do esperado, se tivermos em mente uma política distinta da do escalonador.
Questões relativas à carga do sistema também podem interferir, e por isso a corretude do software tem de ser
garantida. É comum, quando há falhas na garantia da sincronização, que o programa funcione em algumas
execuções e falhe em outras, sem que nada tenha sido modificado. Essa sensibilidade às condições de
execução é praticamente um atestado de problemas e condições de corrida que não foram adequadamente
tratadas.
Por fim, um bom conhecimento do como as threads se comportam é essencial. Isso é importante para evitar
que threads morram inadvertidamente, transformando outras em “zumbis”. Também é um ponto crítico quando
operações de E/S ocorrem, pois são operações que muitas vezes podem bloquear a thread indefinidamente.
Atividade 3
Você está finalizando uma apresentação para um seminário sobre desenvolvimento de sistemas, com foco em
programação multithreading em Java. Para reforçar a importância desse tema, você pediu sugestões aos seus
colegas sobre o motivo de o conhecimento em programação paralela e o uso de threads em Java serem tão
essenciais. As respostas estão listadas nas opções a seguir. Qual delas você escolheria para destacar a
relevância da programação paralela e threads em Java?
Considerações finais
O que você aprendeu neste conteúdo?
• Processamento paralelo e threads em Java.
• Objetos imutáveis.
• Implementação de threads.
Explore +
Como se trata de um assunto rico, há muitos aspectos que convêm ser explorados sobre o uso de threads.
Sugerimos conhecer as nuances da MVJ para melhorar o entendimento sobre como threads funcionam em
Java.
Busque também conhecer mais sobre escalonadores de processo e suas políticas. Veja não apenas como a
MVJ implementa essas funcionalidades, mas como os sistemas operacionais o fazem. Ao estudar o
agendamento de processos de sistemas operacionais e da MVJ, identifique as limitações e os problemas que
podem ocorrer.
Outro ponto importante é conhecer o que a API Java oferece de recursos para programação com threads.
Para isso, uma consulta à documentação da API disponibilizada pela própria Oracle é um excelente ponto de
partida.
Você pode se interessar, inclusive, em conhecer os principais problemas envolvidos em programação paralela.
Aqui mencionamos superficialmente a ocorrência de condições de corrida, mas sugerimos se informar melhor
sobre essa questão e outras, como deadlocks e starvation. Indicamos também que você pesquise problemas
clássicos como o jantar dos filósofos — às vezes apresentado com nomes diferentes, como “filósofos
pensantes”.
Por fim, tome essas sugestões como apenas um começo. Conforme você explorar esses assuntos, outros
surgirão. Estude-os também. Estude sempre. Estude muito!
Referências
ORACLE AMERICA INC. Chapter 17. Threads and Locks. Consultado na internet em: 5 maio 2021.
ORACLE AMERICA INC. Class Thread. Consultado na internet em: 5 maio 2021.
ORACLE AMERICA INC. Enum [Link]. Consultado na internet em: 5 maio 2021.
ORACLE AMERICA INC. Java Thread Primitive Deprecation. Consultado na internet em: 5 maio 2021.
ORACLE AMERICA INC. Semaphore (Java Platform SE 7 ). Consultado na internet em: 5 maio 2021.
ORACLE AMERICA INC. Thread (Java Platform SE 7 ). Consultado na internet em: 5 maio 2021.
SCHILDT, H. Java - The Complete Reference. Nova York: McGraw Hill Education, 2014.