Escolar Documentos
Profissional Documentos
Cultura Documentos
Embora a programação de multiprocessos funcione bem, há desvantagens em seu uso. Primeiro, quando um
processo se divide em dois, há sobreposição entre o armazenamento de dados de um processo e de outro. Como
duas cópias dos dados estão sendo mantidas, mais memória do que o necessário é consumida. Em segundo lugar,
não existe uma maneira fácil para um processo acessar e modificar os dados de outro. No Unix, a comunicação
entre processos (IPC) é usada, criando tubos de dados que permitem que um processo se comunique com outro.
No entanto, não é tão fácil projetar um software que compartilhe dados em um ambiente multiprocessado quanto
em um multithread.
A programação multithread requer uma maneira diferente de ver o software. Em vez de executar uma série de etapas
sequencialmente, as tarefas são executadas simultaneamente - ou seja, muitas tarefas são executadas ao mesmo tempo, em
vez de uma tarefa ter que terminar antes que outra possa começar. Multi-threading, também conhecido como vários threads
de execução, permite que um programa tenha várias instâncias em execução, enquanto usa o mesmo espaço de memória
compartilhada e código (ao contrário da programação de multiprocessos, que usa espaços de endereço de memória
separados, tornando difícil a comunicação entre os processos) . Um aplicativo pode estar executando muitas tarefas
diferentes simultaneamente, e os threads podem acessar variáveis de dados compartilhados para trabalhar de forma
colaborativa.
Você provavelmente já viu esse tipo de comportamento em aplicativos de software, embora talvez não o tenha
reconhecido. Quando programas com uma interface gráfica de usuário são executados, eles normalmente usam
multi-threading. Por exemplo, um aplicativo GUI em execução em uma máquina Windows pode estar executando
alguma tarefa de processamento, como atualizar células em uma planilha ou enviar um documento para a impressora.
Você já se perguntou por que pode mover uma janela, redimensioná-la ou acessar o menu de arquivo, embora o
aplicativo esteja executando alguma outra tarefa? A interface gráfica do usuário (que geralmente usa componentes
GUI do sistema operacional subjacente) está sendo continuamente atualizada, independente do aplicativo real, por um
ou mais encadeamentos GUI que atualizam a exibição da tela e capturam eventos GUI, como cliques e arrastos do
mouse.
132
A maioria dos sistemas operacionais modernos oferece suporte a multitarefa. Embora alguns aplicativos não
aproveitem as vantagens do multithreading e sejam escritos como aplicativos single-threaded, eles ainda estão
sendo executados em um ambiente multithread. O sistema operacional pode permitir que outros aplicativos e outros
threads sejam executados, enquanto o aplicativo de thread único fica alheio. Então, como isso é feito?
A menos que você tenha mais de uma CPU, apenas um único thread pode estar em execução a qualquer momento. O sistema
operacional mantém uma fila de threads e aloca tempo de CPU para eles. No entanto, o tempo deve ser compartilhado -
alocar todo o tempo em um único encadeamento seria injusto, pois impediria que outros encadeamentos fizessem seu
trabalho (isso é conhecido como falta de encadeamento). Um sistema operacional multitarefa preventivo é aquele que
suspende um encadeamento (mesmo que ainda tenha trabalho a fazer) para que outros encadeamentos possam ter tempo
de CPU.
Claro, decidir qual thread deve ter tempo de CPU alocado é complicado. O processo de determinar qual thread executar é
chamado de agendamento. Nem todos os sistemas operacionais alocam tempo de thread de maneira justa, mas para dar um
guia ao sistema operacional, os threads são alocados em um nível de prioridade. Alguns threads operam em um nível muito
alto de prioridade, o que significa que eles recebem o primeiro disparo no momento da CPU, enquanto outros threads operam
em um nível muito baixo de prioridade. Threads de baixa prioridade normalmente não obtêm uma parte justa do tempo de
CPU e podem não ser executados se os threads de prioridade mais alta esgotarem todo o tempo de CPU disponível. Uma vez
que a escolha de qual thread é executado depende do sistema operacional e não do aplicativo, torna-se impossível prever a
ordem de execução ou quanto tempo de CPU será fornecido.
Esse sistema pode parecer anárquico à primeira vista. Se os threads estão acessando e modificando dados ao acaso,
como então um trabalho útil pode ser executado? Deve-se prestar muita atenção ao acesso simultâneo e à
modificação de dados, para evitar que os dados fiquem fora de sincronia. Com um design cuidadoso, no entanto, os
dados podem ser bloqueados, o que impedirá o acesso de leitura enquanto ocorre o acesso de gravação. A
programação multithread pode ser difícil de dominar, mas as recompensas que ela oferece são grandes. Os clientes
de rede não precisam bloquear a GUI se uma conexão de rede for interrompida e os servidores podem processar
vários clientes simultaneamente. Além disso, os threads podem usar variáveis de forma independente e não são
forçados a compartilhar os mesmos dados. Um encadeamento pode, por exemplo, declarar seu próprio conjunto de
variáveis que não disponibiliza para outros encadeamentos (marcando-os como privados ou protegidos),
Java, como muitas linguagens de programação modernas, inclui suporte para aplicativos multithread. Em Java,
threads de execução são representados pelo Java. lang.Thread classe, enquanto o código para tarefas que são
projetadas para serem executadas em um thread separado é representado pelo java.lang.Runnable
interface. É muito importante que os desenvolvedores estejam cientes de ambos.
133
O java.lang.Thread A classe fornece métodos para iniciar, suspender, retomar e parar um encadeamento, bem como
para controlar outros aspectos, como a prioridade de um encadeamento ou o nome associado a ele. A maneira mais
simples de usar o Fio classe é estendê-lo e substituir o corre() método, que é chamado quando o encadeamento é
iniciado pela primeira vez. Substituindo o corre() método, um thread pode ser feito para executar tarefas úteis em
segundo plano.
NOTA
O exemplo a seguir mostra como estender o Fio classe e iniciar várias instâncias em execução, cada uma em
uma thread separada.
// Capítulo 7, Listagem 1
public class ExtendThreadDemo extends java.lang.Thread {
int threadNumber;
tentar
{
// Dormir por cinco mil milissegundos // (5 segundos), para
simular o trabalho sendo feito Thread.sleep (5000);
}
catch (InterruptedException ie) {}
134
// Inicie os dois threads t1.start ();
t2.start ();
}
}
Quando compilado e executado, este exemplo mostra os threads sendo criados e a saída de cada um. Os encadeamentos
ficam suspensos por cinco segundos, para simular a ocorrência de trabalho significativo e, em seguida, são encerrados, o que
resulta no fechamento do encadeamento. Somente quando todos os encadeamentos forem encerrados, o método principal
será encerrado. Duas coisas importantes devem ser observadas neste exemplo.
Primeiro, o corre() método não foi invocado quando o thread foi criado, apenas quando o thread foi iniciado
chamando o começar() método. Embora a diferença seja sutil, não deixa de ser importante. Você pode criar threads
com antecedência e iniciá-los somente quando necessário. Lembre-se de que o objeto thread representa apenas um
thread - os threads são, na verdade, fornecidos pelo próprio sistema operacional. Quando o começar() método de um
thread é chamado, ele envia uma solicitação para lançar um thread separado, que irá chamar o corre() método.
NOTA
O aplicativo principal não chama o corre() método diretamente. Em vez disso, ele
chama começar() para realizar esta operação. Se o seu aplicativo chamar corre()
diretamente, ele não será executado como um segmento separado.
Em segundo lugar, o método principal termina quando os dois threads são iniciados. Não há comando de pausa ou
suspensão emitido no thread principal - embora o aplicativo não seja encerrado. Ele continua (embora não tenha nenhum
trabalho real a fazer e esteja ocioso) até que os dois threads tenham terminado seu trabalho e deixem seu corre() método.
Quando um encadeamento normal (também conhecido como encadeamento do usuário) é criado, espera-se que ele conclua
seu trabalho e não seja encerrado prematuramente. A Java Virtual Machine (JVM) não será encerrada até que todos os
encadeamentos do usuário tenham terminado ou até que uma chamada seja feita para o
System.exit () método, que encerra o JVM abruptamente. Às vezes, no entanto, os threads só são úteis quando outros
threads estão em execução (como o aplicativo real, que eventualmente será encerrado quando o usuário terminar de
usá-lo). Chamamos esses tipos de threads de daemon de threads, em oposição a threads de usuário. Se apenas os
encadeamentos daemon estiverem em execução, a JVM será encerrada automaticamente.
A seguir, veremos os encadeamentos daemon em ação, modificando o método principal da seguinte forma (alteração
indicada em negrito) para especificar os dois encadeamentos (t1 e t2) como encadeamentos daemon.
135
// Inicie os dois threads t1.start ();
t2.start ();
tentar
{
// Dormir por um segundo, para permitir que os threads //
tempo para exibir a primeira mensagem
Thread.sleep (1000);
}
catch (InterruptedException ie) {}
}
Ao estender o Fio classe é uma maneira de criar um aplicativo multithread, nem sempre é a melhor maneira.
Lembre-se de que Java oferece suporte apenas para herança única, ao contrário de linguagens como C ++, que
oferece suporte para herança múltipla. Isso significa que se uma classe estende o
java.lang.Thread classe, ele não pode estender qualquer outra classe - um recurso que muitas vezes é
necessário no design de software Java. A melhor maneira é muitas vezes implementar o java.lang.Runnable
interface.
O Executável interface define um único método, corre() , que deve ser implementado. As classes
implementam essa interface para mostrar que podem ser executadas como um thread de execução
separado. A assinatura precisa para o método de execução é a seguinte:
O Executável interface não define nenhum outro método ou fornece qualquer funcionalidade específica de thread.
Seu único propósito é identificar classes capazes de rodar como threads. Quando um objeto implementando o Executável
interface é passada para o construtor de um encadeamento, e o encadeamento
começar() método é invocado, o corre() método será chamado pelo segmento recém-criado. Quando o corre() o
método termina, a execução do thread é interrompida.
Existem várias vantagens em usar o Executável interface estendendo o Fio aula. A primeira, conforme mencionado
acima, é que um objeto está livre para herdar de uma classe diferente. Segundo, o mesmo Executável objeto pode ser
passado para mais de um thread, portanto, vários threads simultâneos podem estar usando o mesmo código e
agindo nos mesmos dados. Embora esse uso nem sempre seja recomendado, pode fazer sentido em certas
circunstâncias, desde que o devido cuidado seja tomado para evitar conflitos de acesso aos dados. Terceiro,
aplicativos cuidadosamente projetados podem minimizar a sobrecarga, como a criação de um novo Fio
instância requer memória valiosa e tempo de CPU. UMA Executável instância, por outro lado, não incorre na mesma
carga de um encadeamento e ainda pode ser passado para um encadeamento posteriormente para ser reutilizado e
executado novamente, se necessário.
Abaixo está um exemplo de um aplicativo multi-thread que usa o Executável interface em vez de
uma subclasse do Fio aula.
136
// Capítulo 7, Listagem 2
public class RunnableThreadDemo implementa java.lang.Runnable {
Quando o exemplo é compilado e executado, dois threads podem ser vistos imprimindo uma mensagem no
console. O que é muito diferente neste programa, e no anterior, é que apenas um Executável
objeto foi criado, mas dois threads diferentes o executaram. Embora não houvesse dados compartilhados neste
exemplo, em sistemas mais complexos, os encadeamentos devem compartilhar o acesso aos recursos, para evitar
modificação enquanto um recurso está sendo acessado. Isso é obtido sincronizando o acesso aos recursos (discutido
posteriormente neste capítulo).
Conforme mostrado nos dois exemplos anteriores, é relativamente fácil iniciar a execução de uma thread. Existem também
outras maneiras de controlar os fios.
Leitores atentos podem ter notado que sempre que uma chamada para o Fio. dormir (int)
método foi feito em exemplos anteriores, um manipulador de exceção foi usado. Isso ocorre porque o método do
sono coloca um fio para dormir por um longo período de tempo, durante o qual ele geralmente é incapaz de
despertar. No entanto, se um thread deve ser ativado mais cedo, interromper um thread o despertará; isto é
conseguido invocando o interromper() método. Obviamente, isso requer outro encadeamento para manter uma
referência ao encadeamento em espera.
// Capítulo 7, Listagem 3
public class SleepyHead extends Thread {
137
// O método Run é executado quando o thread é iniciado pela primeira vez
public void run ()
{
System.out.println ("Sinto-me com sono. Acorde-me às oito horas
horas");
tentar
{
// Durma por oito horas
Thread.sleep (1000 * 60 * 60 * 8);
thread em espera
sleepy.start ();
tópico
sleepy.interrupt ();
}
}
O único propósito do tópico neste exemplo é dormir por muito tempo. Uma vez que o fio está adormecido, ele não
pode despertar por si mesmo. O único curso de ação é enviar uma mensagem de interrupção de outro tópico.
Execute o exemplo e você verá que o thread está ocioso. O encadeamento primário (executando o método principal)
espera que o usuário pressione "entrar" e, em seguida, envia uma mensagem de interrupção (que será capturada, a
menos que o encadeamento em espera tenha despertado por conta própria e encerrado). O thread secundário é
ativado, exibe uma mensagem e, em seguida, termina, permitindo que o aplicativo seja fechado.
Às vezes, é necessário encerrar um thread antes que sua tarefa seja concluída. Por exemplo, se um cliente de rede
estiver enviando mensagens para um servidor de e-mail em um segundo encadeamento e o usuário quiser cancelar a
operação (talvez para excluir uma chama construída às pressas antes de ser enviada), o encadeamento deve ser
interrompido imediatamente. Um tópico pode enviar uma mensagem de parada para outro tópico, invocando o Thread.stop
() método. Isso requer que o encadeamento de controle (emitindo a mensagem de parada) mantenha uma referência
ao encadeamento que deseja encerrar.
// Capítulo 7, Listagem 4
public class StopMe extends Thread
138
{
// O método Run é executado quando o thread é iniciado pela primeira vez
public void run ()
{
contagem interna = 1;
para (;;)
{
// Imprime a contagem e a incrementa
System.out.print (count ++ + "");
{
// Cria e inicia a contagem através do contador ead
de threads = new StopMe (); counter.start ();
// Interrompe o thread
counter.stop ();
}
}
Uma vez iniciado, o encadeamento neste exemplo exibirá uma contagem crescente, que continuará indefinidamente
sem terminar. Para interromper a discussão, o Thread.stop () método é usado. Às vezes, é absolutamente necessário
interromper um thread, mas esse método deve ser usado com cautela. Ele foi descontinuado na plataforma Java 2,
devido a um problema potencial que pode causar corrupção de dados. Quando um encadeamento é interrompido
dessa maneira, os monitores de acesso que protegem dois encadeamentos de acessar o mesmo recurso
simultaneamente são liberados, mas o próprio recurso pode estar em um estado inconsistente. Embora isso
raramente aconteça, pode ocorrer sem aviso. Por este motivo, é aconselhável que os threads verifiquem
regularmente se devem ou não continuar (por exemplo, verificando o estado de um sinalizador booleano), em vez de
ter outro thread invoque o Pare()
método.
NOTA
139
7.2.3.3 Suspendendo / Retomando Threads
Antes do Java 2, era permitido suspender e retomar os encadeamentos, permitindo que um aplicativo pausasse
os encadeamentos sem interrompê-los permanentemente. Isso foi conseguido usando o Thread.suspend ()
e depois o Thread.resume () métodos. No entanto, esses métodos foram reprovados na plataforma Java 2, pois às
vezes podem causar um conflito (uma situação em que um ou mais encadeamentos aguardam o acesso a um
recurso, mas o bloqueio do recurso não é liberado). Isso pode ocorrer se um encadeamento suspenso bloquear um
monitor e não puder liberá-lo enquanto estiver suspenso. Embora os métodos ainda funcionem, é recomendável que
eles não sejam usados.
Às vezes, um encadeamento pode estar aguardando a ocorrência de um evento ou pode estar entrando em uma
seção de código onde liberar tempo de CPU para outro encadeamento melhorará o desempenho do sistema ou a
experiência do usuário (por exemplo, após realizar um cálculo que deve ser exibido para o usuário e antes de iniciar
outro). Às vezes, é vantajoso, nessas situações, para um encadeamento ceder tempo de CPU para outro
encadeamento em vez de hibernar por um longo período. Por exemplo, enquanto espera que os dados se tornem
disponíveis a partir de um InputStream , um encadeamento pode gerar tempo de CPU em vez de entrar em suspensão.
Nesta situação, o estático produção() método pode ser usado em vez do
dormir() método. Por exemplo, para que o encadeamento atualmente em execução produza tempo de CPU, o seguinte
método pode ser invocado:
Thread.yield ();
Como esta instrução leva apenas uma única linha de código e não requer interação com outro encadeamento,
nenhum exemplo é fornecido. No entanto, é um método útil de se estar ciente, particularmente em sistemas
complexos onde muitos threads estão lutando pelo tempo de CPU. Lembre-se também de que é um método estático
que afeta apenas o encadeamento em execução - um aplicativo não pode render o tempo de um encadeamento
específico.
Às vezes, é necessário esperar até que um encadeamento termine sua tarefa (por exemplo, para recuperar
os resultados da tarefa invocando um método ou lendo uma variável de membro). Para determinar se um
tópico morreu (ou seja, se o corre() método terminou), o Está vivo() método, que retorna um
boleano valor, pode ser invocado. Mas verificar continuamente o valor retornado por esse método
(conhecido como polling) e, em seguida, dormir ou render, é um uso muito ineficiente do tempo da CPU.
Uma maneira muito melhor é usar o Thread.join () método, que espera que um fio morra. Também existe
uma versão sobrecarregada deste método, que toma como parâmetro um longo valor. Esta versão aguarda
a morte do thread ou o número especificado de milissegundos, o que ocorrer primeiro.
// Capítulo 7, Listagem 5
public class WaitForDeath extends Thread {
{
Thread.sleep (5000);
}
140
catch (InterruptedException ie) {}
}
{
// Cria e inicia o encadeamento Thread dying = new
WaitForDeath (); dying.start ();
7.3 Sincronização
Uma consideração importante ao projetar aplicativos multithread é o conflito sobre o acesso aos dados. Se
dois encadeamentos estão lutando pelo mesmo recurso e um mecanismo para resolver conflitos de acesso
não é colocado em prática, a integridade do aplicativo está em jogo. Há dois mecanismos integrados à
linguagem Java para impedir o acesso simultâneo a recursos: sincronização em nível de método e
sincronização em nível de bloco.
A sincronização em nível de método evita que dois threads executem métodos em um objeto ao
mesmo tempo. Os métodos que devem ser "thread-safe" são marcados como sincronizados.
Quando um método sincronizado de um objeto é chamado, um thread tira um bloqueio de objeto
ou monitor. Se outro encadeamento tentar executar qualquer método sincronizado, ele
descobrirá que está bloqueado e entrará em um estado de suspensão até que o bloqueio no
monitor de objeto seja liberado. Se vários threads tentarem executar um método em um objeto
bloqueado, uma fila de threads suspensos se formará. Quando o encadeamento que instituiu o
bloqueio retorna do método, apenas um dos encadeamentos enfileirados pode acessar o objeto -
a liberação de um monitor não permite que mais de um objeto tire um novo monitor. Deve-se
notar, no entanto,
A sincronização em nível de método é um mecanismo comum para sincronizar o acesso aos recursos. Ao
projetar aplicativos multithread ou classes que precisam ser thread-safe para uso em ambientes multithread,
os métodos podem ser sincronizados para evitar perda ou corrupção de dados. A menos que os dados
possam ser acessados atomicamente (em uma operação, sem a possibilidade de uma thread ser suspensa
pela JVM e o controle ser fornecido a outra), a sincronização é necessária. Considere o exemplo simples de um
contador (por exemplo, quantas vezes uma ação como um hit em uma página da Web ocorreu) armazenado
em um arquivo. Este contador pode ser incrementado (lendo o valor atual e escrevendo um novo) ou lido por
múltiplos threads. Se um thread tentar incrementar o valor do
141
contador antes que outro thread conclua a modificação do contador, seu valor pode ser definido por um e
sobrescrito pelo outro. Isso significa que o contador leria um valor inválido. Pior ainda, se duas tentativas de
sobrescrever o valor forem feitas, o arquivo pode estar corrompido. Se os métodos para acessar e modificar o
valor do contador fossem sincronizados, no entanto, apenas um thread poderia realizar uma operação de
gravação em um determinado momento.
O sincronizado A palavra-chave é usada para indicar que um método deve ser protegido por um monitor. Todos os
métodos que podem ser afetados pelo acesso simultâneo devem ser marcados como sincronizados. No entanto, essa
palavra-chave deve ser usada com moderação, pois tem uma desvantagem de desempenho. Embora seja adequado
para aplicativos multithread, em um contexto de thread único resulta em perda de tempo de CPU.
.........
}
............
}
}
Vejamos um exemplo prático de conflitos de thread e como eles podem ser resolvidos usando métodos
sincronizados. Suponha que temos um contador que pode ser incrementado e exibir um valor. Se o método
que fornece acesso ao contador não é seguro para thread e leva algum tempo para ser concluído, dois ou
mais threads podem acessá-lo ao mesmo tempo e sobrescrever cada incremento. Antes que o valor do
contador possa ser armazenado, um segundo thread pode escrever um novo, que por sua vez é sobrescrito
(ver Figura 7-3 ) Isso fica ainda mais confuso quando uma leitura é feita. Uma vez que uma atualização foi
perdida, uma contagem imprecisa é obtida. Se alterações frequentes forem feitas no contador, ele se tornará
cada vez mais impreciso.
Figura 7-3. Acesso simultâneo e modificação de dados por threads leva a dados
corrupção.
A solução é tornar o contador thread-safe, sincronizando cada método que executa uma operação de leitura ou
gravação. Se um método sincronizado for usado, apenas uma thread pode atualizar o valor a qualquer momento
(veja Figura 7-4 ) O encadeamento que primeiro invoca um método sincronizado bloqueia o monitor do objeto, que é
liberado apenas quando o método é encerrado. Nenhum outro thread pode acessar qualquer método sincronizado
do objeto contador (embora se vários contadores forem usados, este
142
restrição se aplica apenas a instâncias de contadores individuais, e não a Balcão própria classe). Por esse motivo, os
métodos sincronizados devem ser tão breves quanto possível - threads que vão dormir dentro de um método
sincronizado farão com que todas as outras threads que tentam invocar métodos sincronizados naquele objeto
sejam suspensas.
Agora veremos a sincronização no nível do método em ação. A classe a seguir define um contador com
métodos de modificação e acesso sincronizados.
Balcão público ()
{
countValue = 0;
}
{
Thread.sleep (5);
}
catch (InterruptedException ie) {}
contagem = contagem + 1;
countValue = count;
143
}
A próxima classe é um aplicativo que usa vários threads com um único contador. Devido ao
sincronizado palavra-chave, a modificação simultânea do valor do contador é impossível, mas compilar e
executar o exemplo sem a presença do sincronizado palavra-chave revela alguns resultados interessantes.
Conforme previsto, a contagem torna-se imprecisa. No entanto, como a operação de gravação é tão rápida,
ela precisa ser desacelerada em máquinas mais rápidas para permitir que outros threads entrem no aumentarContagem
() método. O ajuste do valor do sono pode ser necessário em máquinas muito rápidas.
// Capítulo 7, Listagem 7
public class CountingThread implementa Runnable {
Counter myCounter;
int countAmount;
meuContador = contador;
countAmount = amount;
}
144
}
}
Três threads são lançados, que incrementam o contador 10 vezes cada. Isso significa que, quando o
sincronizado palavra-chave estiver presente, um valor total de 30 será retornado pelo contador
getCount () método. Sem o sincronizado palavra-chave, cada encadeamento sobrescreve o outro e a contagem
resultante é significativamente menor.
A sincronização em nível de método é um meio eficaz de impedir o acesso simultâneo a recursos. Mas e se o
recurso não foi projetado como thread-safe e é uma classe preexistente que o desenvolvedor não pode
modificar (como uma classe na API Java ou uma biblioteca de terceiros)? A sincronização em nível de bloco,
neste caso, é a melhor opção.
A sincronização em nível de bloco usa o sincronizado palavra-chave, mas em vez de colocar um bloqueio em torno de
métodos específicos, um bloqueio é colocado em torno de blocos de código. Um bloco de código é sincronizado com
um objeto específico e qualquer thread que tente entrar nesse bloco de código é bloqueado, até que o monitor do
objeto especificado seja liberado. O seguinte snippet de código mostra a sintaxe para um bloco sincronizado:
sincronizado (Objeto o)
{
......
}
A sincronização em nível de bloco bloqueia em um objeto específico. Isso significa que vários blocos podem proteger
o acesso ao mesmo objeto, portanto, a sincronização em nível de bloco pode ser aplicada no código de thread
sempre que um objeto é acessado ou modificado. O exemplo a seguir demonstra a sincronização em nível de bloco. É
uma variação do exemplo anterior, mas em vez de criar uma classe separada para representar um contador, uma
variável de instância é usada. O acesso e modificação da variável ocorre dentro do corre() método de um thread,
portanto, a sincronização em nível de método não pode ser usada. Em vez disso, a sincronização em nível de bloco
protege a contagem e garante que, quando a contagem for gravada em um String Buffer ( que é uma string que pode
ser anexada), é escrito na ordem correta.
// Capítulo 7, Listagem 8
public class SynchBlock implementa Runnable {
StringBuffer buffer;
contador interno;
public SynchBlock ()
{
buffer = novo StringBuffer (); contador = 1;
}
public void run ()
{
sincronizado (buffer)
{
System.out.print ("Iniciando bloco sincronizado"); int tempVariable =
counter ++;
int tempVariable = counter ++;
145
System.getProperty ("line.separator");
tentar
{
Thread.sleep (100);
}
catch (InterruptedException ie) {}
buffer.append (mensagem);
System.out.println ("... finalizando bloco sincronizado");
}
}
// Espere que todos esses threads terminem t1.join (); t2.join ();
t3.join (); t4.join ();
System.out.println (block.buffer);
}
}
Como a comunicação multiprocesso, que usa canais para enviar dados de um processo para outro, os threads
também podem enviar dados diretamente de um thread para outro. Isso é obtido usando tipos especiais de fluxos
de entrada e saída, que são vinculados. Ao passar uma das extremidades do tubo para outro encadeamento, esse
encadeamento pode ouvir ou falar com outro encadeamento. Na verdade, não há nenhuma restrição que impeça o
uso de dois canais - um segmento pode até ter comunicação bidirecional com outro (consulte Figura 7-5 )
Figura 7-5. A comunicação canalizada é apenas uma via, mas podem ser usados dois canos.
146
A comunicação por meio de tubos não é muito diferente de usar qualquer outra forma de fluxo de entrada ou fluxo
de saída. Outros fluxos também podem ser conectados a um tubo, para facilitar a leitura e a escrita. O exemplo a
seguir demonstra o uso de canais na comunicação de thread.
import java.io. *;
// Capítulo 7, Listagem 8
public class PipeDemo extends Thread {
Saída PipedOutputStream;
tentar
{
// Crie um tubo para escrever
PipedOutputStream pout = new PipedOutputStream ();
// Inicie o thread
pipedemo.start ();
147
{
System.err.println ("Erro de tubulação" + e);
}
}
// Imprimir mensagem
p.println ("Olá de outro encadeamento, via tubos!");
// Fechar o stream
p.close ();
}
catch (exceção e)
{
// nenhum código requerido}
}
}
A execução deste exemplo criará um canal unilateral entre o encadeamento do aplicativo principal e um segundo
encadeamento que envia uma mensagem de texto pelo tubo. O tubo deve ser construído antes do encadeamento
ser iniciado, entretanto, e deve ser passado de alguma forma para o encadeamento, para que tenha uma referência
a um objeto de tubo. Nesse caso, o pipe é passado para o construtor do thread - uma maneira conveniente de
inicializar o thread e colocar o pipe lá.
Um requisito comum na programação multithread é que um thread não pode prosseguir até a
conclusão de uma tarefa por outro thread. Às vezes, um thread estará produzindo informações ou
usando recursos. Outras vezes, a ordem de execução é importante e uma tarefa não pode ser realizada
antes que outra seja concluída. Embora seja possível que um thread espere até que outro morra
(indicando, assim, que o trabalho foi concluído) usando o Thread.join () método, e se um encadeamento
executa uma tarefa contínua e nunca termina?
A solução é notificar outros threads de que uma tarefa foi concluída. Os threads esperam até serem
notificados e a notificação pode ser um processo repetido (com vários ciclos de espera e notificação). Isso
permite que os encadeamentos sincronizem suas ações e comuniquem que um evento crítico ocorreu, sem
exigir a complexidade extra de comunicação baseada em tubos ou métodos de chamada. Às vezes, um
thread pode nem saber exatamente quais threads estão aguardando sua conclusão, portanto, um tipo
especial de notificação é usado.
Cada objeto Java herda do java.lang.Object classe (a superclasse de cada objeto) a capacidade de manter uma fila de
threads aguardando a liberação de um bloqueio de objeto e de notificar uma ou mais threads em espera de que o
objeto foi liberado. Isso fornece uma ótima maneira de notificar um encadeamento de que ocorreu um evento e
de os encadeamentos aguardarem indefinidamente (ou por um período limitado de tempo) até que a notificação
seja enviada.
Para que os threads esperem por um período indefinido de tempo, o Object.wait () método é usado. Também existe uma
versão sobrecarregada desse método, que espera por um período de tempo limitado (especificado em milissegundos). Antes
de o esperar() método pode ser chamado, no entanto, o encadeamento deve conter um bloqueio no monitor do objeto. Para
obter um bloqueio no monitor de um objeto, ele deve estar executando um
148
método sincronizado ou usando um bloco sincronizado. Quando o bloqueio é liberado, outro encadeamento pode
obtê-lo - sem isso, o encadeamento aguardará indefinidamente.
Uma vez o esperar() método é executado, o monitor é liberado e o encadeamento é suspenso até que um
a chamada é feita para o Object.notify () ou Objeto. notificar todos () método. Para despertar
threads em espera, outra thread pode chamar qualquer um dos métodos. No entanto, o notificar () método irá notificar apenas
um único thread, mesmo se vários threads estiverem esperando. Também não há escolha sobre qual encadeamento será
ativado (isso é determinado pela implementação da JVM, portanto, você não pode contar com, por exemplo, uma fila FIFO),
portanto, é aconselhável que o notificar tudo () método é usado se você deseja notificar um segmento específico.
// Capítulo 7, Listagem 9
public class WaitNotify extends Thread {
tentar
{
System.in.read ();
}
catch (java.io.IOException ioe)
{
// nenhum código requerido}
O exemplo é bastante simples. O encadeamento do aplicativo principal espera até ser notificado pelo segundo
encadeamento de que ocorreu um evento. Até que o usuário pressione a tecla "enter", o segundo tópico não enviará
a mensagem de notificação. Embora o aplicativo seja encerrado assim que a notificação for enviada e recebida, uma
série de esperar() / notificar ( ) poderiam ocorrer ciclos, para indicar eventos irregulares, mas de ocorrência frequente.
149
7.5 Grupos de Tópicos
Embora os encadeamentos possam ser criados e executados individualmente, às vezes é mais fácil trabalhar com
encadeamentos como um grupo, em vez de um por vez. Operações que afetam threads, como suspender e retomar
threads, pará-los a frio ou interrompê-los, podem ser realizadas em threads individuais, mas isso requer que os
desenvolvedores mantenham uma lista de threads (usando uma estrutura de dados como um vetor ou um array) .
Quando uma operação deve ser realizada, cada um dos encadeamentos nesta lista deve ser percorrido e, em
seguida, executado. Isso cria trabalho extra para os desenvolvedores, exigindo um código mais complexo. Uma
alternativa mais fácil é agrupar threads e aplicar uma operação no grupo, em vez de nos elementos individuais do
grupo.
A API Java fornece suporte para grupos de threads na forma de ThreadGroup aula. O propósito do ThreadGroup classe
deve representar uma coleção de threads e fornecer métodos que atuam como atalhos para invocar métodos
em threads individuais na coleção. Ele também fornece uma maneira de reunir informações sobre threads
comumente relacionados, como o número de threads em um grupo e referências aos threads armazenados
em um grupo. Se o acesso a threads individuais for necessário, ele pode ser obtido no ThreadGroup , em vez de
ter que criar alguma outra estrutura de dados para atuar como um contêiner.
Quando uma JVM é iniciada pela primeira vez e um aplicativo Java é executado, o encadeamento do aplicativo principal irá
executar a Principal() método. Depois disso, o aplicativo fica livre para criar grupos de threads, ou threads individuais não
associados a um grupo específico. Um grupo pode ser composto de quantas threads forem necessárias, e pode ser
adicionado dinamicamente a qualquer ponto durante a execução de um programa (veja Figura 7-
6 ) No entanto, não há como retirar um thread (ou um grupo de threads) de um grupo. Observe, também, que em ambientes
controlados, como gerenciadores de segurança customizados, applets em execução em um navegador ou código em
execução em um mecanismo de servlet, podem ser colocadas restrições na criação de novos grupos de encadeamentos ou
no acesso aos existentes.
Figura 7-6. Os grupos de threads podem adicionar threads adicionais a qualquer momento.
150
Além disso, um grupo de tópicos pode conter grupos de tópicos adicionais, que são contados como subgrupos
( Figura 7-7 ) Operações como parar ou suspender podem ser realizadas nos subgrupos ou no grupo pai.
Quando uma operação é aplicada ao grupo pai, ela fará com que a operação seja propagada para cada
subgrupo, que por sua vez, passará a operação para cada thread dentro daquele subgrupo. Isso significa que
uma única operação pode ser chamada em várias, dezenas ou até centenas de threads. Isso torna o design
muito mais limpo - não há necessidade de uma quantidade infinita de loops iterando em uma lista de threads.
A inclusão de subgrupos permite um controle mais refinado sobre as operações de rosca, pois os grupos
selecionados podem ser acionados enquanto outros são deixados intactos.
Figura 7-7. Os grupos de tópicos podem conter tópicos e subgrupos, que por sua vez podem
incluir mais grupos e tópicos.
Construtores
Por exemplo, para criar um grupo e subgrupo, o seguinte código pode ser usado:
151
ThreadGroup pai = novo ThreadGroup ("pai");
Subgrupo ThreadGroup = novo ThreadGroup (pai, "subgrupo");
Depois de criado, um grupo de encadeamentos pode ser usado da mesma maneira que um encadeamento normal - pode ser
suspenso, retomado, interrompido e interrompido chamando o método apropriado. No entanto, uma etapa adicional deve
ser realizada antes que o encadeamento possa ser usado com eficácia - ele deve conter encadeamentos. Afinal, um grupo de
encadeamentos vazio não tem muita utilidade.
Construtores
Threads são associados a um determinado grupo no momento de sua criação. Portanto, não é possível
atribuir um tópico a um grupo posteriormente, ou mover um tópico de um grupo para outro. Existem
três construtores para o Java. lang. Fio classe que aceita grupos de threads como parâmetro:
NOTA
Uma vez que os threads são associados ao grupo, as operações do grupo podem ser realizadas, o número de
threads / grupos que estão ativos pode ser verificado e uma lista de threads pode ser obtida, simplesmente
invocando o apropriado ThreadGroup método.
Métodos
Os seguintes métodos do ThreadGroup classe são públicas, a menos que indicado de outra forma.
152
• enumerar int (Thread [] threadList) arremessa java.lang.Security -
Exceção - coloca na matriz especificada uma referência a cada um dos encadeamentos ativos neste grupo e
a qualquer um de seus subgrupos. Se, no entanto, o tamanho da matriz não for grande o suficiente para
acomodar o número de threads ativos, não
ArrayOutOfBoundsException será lançado - o método, em vez disso, deixará de fora os threads
extras e o aplicativo pode permanecer alheio a isso. Por esse motivo, um array suficientemente
grande deve ser usado. O número de threads copiados na matriz é retornado por este método,
não o número de threads que deve foram copiados. Para determinar o número de threads
ativos (com o propósito de criar uma matriz), uma chamada deve ser feita para o ThreadGroup.
contagem ativa () método. Tópicos adicionais podem, no entanto, ser adicionados ao grupo,
então esta chamada deve ser feita antes de invocar o enumerar método.
• void resume () - método obsoleto que retoma todas as threads suspensas no grupo e
quaisquer subgrupos. O motivo da suspensão é porque o Thread.resume ()
método foi marcado como depreciado na plataforma Java 2.
153
• void setDaemon (sinalizador booleano) arremessa java.lang.Security
Exceção —Modifica o sinalizador do daemon do grupo de encadeamentos. Um valor "true" torna o grupo de
encadeamentos um grupo de daemons, o que significa que ele pode ser destruído quando todos os
encadeamentos ativos são encerrados.
• void setMaxPriority (prioridade interna) arremessa java.lang.Security
Exceção - atribui um novo nível máximo de prioridade para este grupo. Este método não redefine a
prioridade dos threads existentes (ou seja, um thread de prioridade mais alta que faz parte do grupo
não terá sua prioridade reduzida, mas pode não aumentar sua prioridade ainda mais).
• void stop () arremessa java.lang.SecurityException - método obsoleto que
invoca o Thread.stop () método para todos os tópicos neste grupo e quaisquer subgrupos. Enquanto
o Thread.stop () método está obsoleto na plataforma Java 2, este método também está obsoleto.
// Capítulo 7, Listagem 11
public class GroupDemo implementa Runnable {
// Cria um grupo que é filho de outro grupo de threads ThreadGroup subgroup = new
ThreadGroup (parent, "subgroup");
// Cria alguns threads no pai e na classe de subgrupo Thread t1 = new Thread (pai,
new GroupDemo ()); t1.start ();
154
// Fazer nada
para(;;)
{
Thread.yield ();
}
}
}
Em Java, uma classificação numérica especifica a prioridade do encadeamento, com 10 sendo o tipo de prioridade mais alta e 1 sendo o
mais baixo. Algumas prioridades de thread são definidas como variáveis de membro estático do
java.lang.Thread classe, um atalho conveniente que elimina a necessidade de lembrar um valor
numérico. Tabela 7.1 mostra as prioridades numéricas do thread e atalhos associados.
10 Thread.MAX_PRIORITY
7
8
7
6
5 Thread.NORM_PRIORITY
4
3
2
1 Thread.MIN_PRIORITY
Para definir a prioridade do thread, o Thread.setPriorityMethod (int) método é usado. Por exemplo,
para definir um thread com o nível de prioridade mínimo, o seguinte código seria usado:
Um encadeamento que deseja determinar sua prioridade de encadeamento atual (por exemplo, para ver se
é alta o suficiente e tomar uma ação corretiva se não for), pode fazê-lo invocando o
Thread.getPriority () método. Este método retorna um valor int, que indica o
155
prioridade do segmento. Por exemplo, para obter a prioridade do thread em execução no momento, o
seguinte código pode ser usado:
Às vezes, pode ser necessário limitar a prioridade máxima que um thread pode solicitar (por exemplo, se
estiver executando um código não confiável baixado da rede). É possível instalar um gerenciador de
segurança personalizado, o que geraria um Exceção de segurança , mas isso envolve uma quantidade significativa
de esforço. Uma alternativa muito mais fácil é criar um ThreadGroup ( abordado na seção anterior), e atribua um
nível máximo de prioridade a este grupo. Ao criar um encadeamento, o grupo de encadeamentos é
especificado. Qualquer thread que solicitar uma prioridade mais alta não a receberá, mas também não será
gerada uma mensagem de erro. Isso cria uma solução limpa para o problema.
Para atribuir uma prioridade máxima de thread para um grupo, o ThreadGroup. definir prioridade máxima (int) método
é usado. Por exemplo, para definir uma prioridade de thread de 8, o seguinte código seria usado:
7,7 Resumo
Uma compreensão da programação multithread é importante para qualquer tipo de programação de aplicativo ou
applet, mas particularmente para desenvolvimento de software para ambientes de rede. As redes são lentas e não
confiáveis, portanto, o código de rede frequentemente precisará ser executado em uma thread separada daquela da
interface do usuário, para evitar que um sistema trave ou paralise devido a mudanças no estado da rede. Além disso,
a maioria dos softwares de rede interage com vários clientes e / ou servidores e, a menos que as operações sejam
extremamente rápidas (como receber e despachar um pacote UDP), vários threads serão necessários para que as
interações possam ocorrer simultaneamente.
Destaques do capítulo
156
A execução simultânea de software, no entanto, apresenta complexidades. Sincronização de acesso a dados,
comunicação entre threads e agendamento de threads são componentes difíceis para o programador dominar e,
portanto, até mesmo desenvolvedores experientes encontram problemas quando confiam demais na previsão da
ordem de execução da thread. Em muitas situações, é importante que a sincronização de acesso aos recursos seja
usada para preservar a integridade dos dados e a ordem das operações. Além disso, a comunicação entre os
encadeamentos pode ser empregada para auxiliar os encadeamentos na colaboração para concluir tarefas.
157
Capítulo 8. Implementando Protocolos de Aplicativos
Este capítulo apresenta uma visão geral dos protocolos de aplicativos comuns da Internet e fornece
exemplos de código de três implementações de protocolo em Java. Aqui, usamos as técnicas abordadas em
Capítulo 6 para criar software cliente e servidor usando sockets que suportam protocolos populares da Internet.
Um protocolo de aplicativo facilita a comunicação entre aplicativos, usando os serviços de camadas de modelo
de Open Systems Interconnection (OSI) de nível inferior (por exemplo, camadas de rede e físicas). Quando
você verifica seu e-mail, navega em um site da Web, joga ou baixa arquivos da Internet, o software que você
executa está usando um protocolo de aplicativo para comunicação.
Para que os aplicativos interoperem, a implementação de protocolos de aplicativos deve ser precisa. Você não pode
ter um aplicativo falando de uma maneira e outro aplicativo incapaz de entender ou interpretar a mensagem - caso
contrário, os muitos milhares de aplicativos de software em execução nos muitos milhões de computadores
existentes não poderiam se dar bem. Isso não quer dizer que toda implementação oferecerá suporte a todos os
recursos de um protocolo, ou que eles serão implementados da mesma maneira, na mesma linguagem ou na mesma
máquina. No entanto, as implementações de protocolo devem, pelo menos externamente, se comportar da mesma
maneira e, quando não podem atender a uma solicitação ou oferecer suporte a um recurso, devem comunicar isso
de forma eficaz, usando um processo comumente compreendido.
No início da história da Internet, um sistema foi projetado para facilitar a distribuição de especificações de
protocolo novas e atualizadas para implementadores de protocolo. O projeto de um protocolo de rede tende
a ser um processo evolutivo envolvendo muitos colaboradores, com rascunhos iniciais disponibilizados ao
público e reescritas realizadas. Quando o protocolo está quase concluído e pronto para ser implementado
pelos aplicativos, ele é publicado como um documento de solicitação de comentário (RFC).
A maioria dos protocolos populares da Internet em uso hoje são publicados como RFCs. Cada RFC detalha um único
protocolo ou ideia sobre a Internet e recebe um número de identificação (por exemplo, RFC 1945 para HTTP / 1.0).
Esses documentos são extremamente detalhados e contêm todas as informações necessárias para a implementação
do protocolo. No entanto, nem todo RFC cobre um protocolo - alguns são visões gerais, notas sobre a arquitetura da
Internet ou sugestões para revisões de protocolos existentes.
NOTA
Há até mesmo uma tentativa ocasional de humor em documentos RFC - para aqueles
com interesse em todas as coisas Java, tanto a linguagem quanto a bebida, você
pode querer dar uma olhada no RFC 2324, o Hyper Text Coffee Pot Control Protocol.
158