Buscar

Java na pratica.pdf

Prévia do material em texto

JAVA NA PRÁTICA
Tópicos Avançados
JDBC Threads Redes RMI CORBA Servlets JSP
Alcione de Paiva Oliveira
Vinícius Valente Maciel
8 de Novembro de 2003
Direitos Autorais
Todos os direitos sobre esta obra estão reservados para os auto-
res do livro. Texto registrado na Biblioteca Nacional: registro
297.373, livro 540, folha 33.
O48J
OLIVEIRA, Alcione de Paiva
Java na prática, tópicos avançados: RMI CORBA
JDBC threads redes servlets JSP / Alcione de Paiva Oliveira,
Vinícius Valente Maciel. - Viçosa : Fábrica de Livros Ed.,
2003.
225 p. il.
1. Linguagem de programação. 2. Java. I. MACIEL,
Vinícius Valente. II. Título.
CDD: 005.133
CDU: 519.682
Sobre os Autores
Alcione de Paiva Oliveira é Doutor em Informática pela
PUC-Rio, Mestre em Ciências pelo Instituto Militar de Engenha-
ria e Bacharel em Oceanografia pela UERJ. Ex-diretor técnico
da INFAX Tecnologia e Sistemas e ex-coordenador do curso de
Engenharia de Computação do Instituto Militar de Engenharia.
Atualmente exerce o cargo de professor Adjunto do Departamento
de Informática da Universidade Federal de Viçosa. Suas áreas de
interesse são Inteligência Artificial, Linguagens de Programação e
Engenharia de Software.
Vinícius Valente Maciel é Mestrando em Ciência da Com-
putação pela Universidade Federal Fluminense e Bacharel em Ci-
ência da Computação pela Universidade Federal de Viçosa. Atu-
almente exerce o cargo de Analista de Sistemas da INFAX Tec-
nologia e Sistemas Ltda. Suas áreas de interesse são Especifica-
ção Formal de Sistemas, Linguagens de Programação, Sistemas de
Tempo-Real e Engenharia de Software.
AGRADECIMENTOS
A elaboração do presente trabalho contou com a colaboração di-
reta e indireta de diversas pessoas. Primeiramente gostaríamos de
agradecer nossas mulheres e companheiras Alexandra e Andréia
que nos apoiaram (e nos toleraram) em todos os momentos. Da
mesma forma gostaríamos de agradecer a nossos pais que sem-
pre estão ao nosso lado. Não podemos esquecer também todos os
nossos alunos que ajudaram com sugestões quando este material
ainda era uma apostila. Finalmente, gostaríamos de agradecer a
todos que direta e indiretamente ajudaram na concretização deste
sonho.
PREFÁCIO
O propósito deste livro é tentar preencher um espaço não coberto
pelos livros relacionados com a linguagem Java. Não existe, até
o momento, um livro que aborde um conjunto de tópicos avança-
dos da linguagem e que seja, ao mesmo tempo, didático e prático.
De modo a promover a didática procuramos nos ater aos princi-
pais aspectos dos assuntos tratados, sem tentar esgotar todos os
aspectos do tema. Já o lado prático advém do uso intensivo de
exemplos. Além disso, existe um exemplo, o da agenda eletrônica,
que é apresentado com um grau crescente de complexidade, o que
permite que o leitor dirija sua atenção para as novas técnicas que
estão sendo introduzidas. O exemplo da agenda eletrônica possui
um grau de complexidade suficiente para ser usado como embrião
de sistemas mais sofisticados.
O livro aborda os recursos da linguagem para o tratamento de
concorrência, acesso a banco de dados, programação em redes e
programação para a Web. Estes são assuntos que não são abor-
dados em muitos livros com um número bem maior de páginas.
Mesmo os livros existentes em tópicos avançados cobrem apenas
um ou dois dos tópicos mencionados.
Esperamos que o livro cumpra o objetivo proposto e permita
que os leitores possam tirar o máximo de proveito da linguagem
Java.
LISTA DE SIGLAS
ASP
ActiveServer Pages
AWT Abstract Window Toolkit
CGI
Common Gateway Interface
GUI
graphical user interface
CORBA
Common Object Request Broker Architecture
CPU
Central Processing Unit
DMZ Demilitarized Zone
FAPESP
Fundação de Amparo à Pesquisa do Estado de
São Paulo
HTML
Hypertext Markup Language
HTTP
Hypertext Transfer Protocol
IP Internet Protocol
JDBC
Java Database Connectivity
JDK
Java Development Toolkit
JSP
Java Server Pages
MIME
Multipurpose Internet Mail Extensions
MVC modelo-visão-controle
ODBC
Open Database Connectivity
ORB
Object Request Broker
PHP
Personal Home Pages
RMI Remote Method Invocation
TCP Transmission Control Protocol
UDP
User Datagram Protocol
URI
Uniform Resource Identifier
URL
Uniform Resource Locator
Conteúdo
1 Introdução 1
1.1 Convenções . . . . . . . . . . . . . . . . . . . . . . 6
2 Concorrência 7
2.1 Criando threads em Java . . . . . . . . . . . . . . . 10
2.1.1 Usando a interface Runnable . . . . . . . . 13
2.2 A classe Thread . . . . . . . . . . . . . . . . . . . . 14
2.2.1 Variáveis públicas . . . . . . . . . . . . . . 17
2.3 Ciclo de Vida dos Threads . . . . . . . . . . . . . . 18
2.3.1 sleep(), yield(), join(), stop(), suspend() e
resume() . . . . . . . . . . . . . . . . . . . . 19
2.4 Daemon Threads . . . . . . . . . . . . . . . . . . . 23
2.5 A Influência do Sistema Operacional sobre os Threads 25
2.5.1 Forma de escalonamento de threads . . . . 26
2.5.2 Relacionamento entre os níveis de priorida-
des definidas na linguagem Java e os níveis
de prioridades definidas nos Sistemas Ope-
racionais . . . . . . . . . . . . . . . . . . . . 27
2.6 Compartilhamento de Memória e Sincronização . . 28
2.6.1 Atomicidade de Instruções e Sincronização
do Acesso à Sessões Críticas . . . . . . . . . 32
2.6.2 Comunicação entre Threads: wait() e notify() 39
3 Programação em rede 55
3.1 Conceitos Sobre Protocolos Usados na Internet . . 55
3.1.1 TCP . . . . . . . . . . . . . . . . . . . . . . 57
v
3.1.2 UDP . . . . . . . . . . . . . . . . . . . . . . 58
3.1.3 Identificação de Hosts (Número IP) . . . . . 58
3.1.4 Identificação de Processos (Portas) . . . . . 59
3.2 Programação em Rede com Java . . . . . . . . . . 60
3.2.1 Comunicação Básica Entre Aplicações . . . 61
3.2.2 Comunicação orientada a conexão (cliente) 61
3.2.3 Comunicação orientada à conexão (servidor) 63
3.2.4 Comunicação Sem Conexão (UDP) . . . . . 66
3.2.5 Comunicação por meio de URL . . . . . . . 69
3.2.6 Manipulando URLs em Java . . . . . . . . 70
3.2.7 Comunicando por meio de URLConnection 72
4 Acesso a Banco de Dados 75
4.1 Modelos de Acesso a Servidores . . . . . . . . . . . 76
4.2 Tipos de Drivers JDBC . . . . . . . . . . . . . . . 77
4.2.1 Obtendo os Drivers JDBC . . . . . . . . . . 79
4.3 Preparando o Banco de Dados . . . . . . . . . . . 79
4.4 Exemplo Inicial . . . . . . . . . . . . . . . . . . . . 84
4.4.1 Carregando o Driver . . . . . . . . . . . . . 85
4.4.2 Estabelecendo a conexão . . . . . . . . . . . 85
4.4.3 Criando e Executando Comandos . . . . . . 86
4.5 Recuperando Valores . . . . . . . . . . . . . . . . . 87
4.6 Trabalhando com Metadados . . . . . . . . . . . . 89
4.7 Trabalhando com datas . . . . . . . . . . . . . . . 91
4.8 Transações e Nível de Isolamento . . . . . . . . . 92
4.8.1 Transação . . . . . . . . . . . . . . . . . . . 92
4.8.2 Níveis de isolamento . . . . . . . . . . . . . 94
4.9 Prepared Statements . . . . . . . . . . . . . . . . . 97
4.10 Procedimentos Armazenados (Stored Procedures) . 98
4.11 Agenda Eletrônica versão JDBC . . . . . . . . . . 100
4.12 Como configurar a ponte JDBC-ODBC . . . . . . 107
5 RMI 111
5.1 Arquitetura RMI . . . . . . . . . . . . . . . . . . . 112
5.2 Criando nossa agenda distribuída . . . . . . . . . . 112
5.2.1 Passo a Passo . . . . . . . . . . . . . . . . . 112
5.2.2 Implementando interface do objeto remoto 113
5.2.3 Escrevendo objeto remoto . . . . . . . . . . 114
5.2.4 Gerando Stub . . . . . . . . . . . . . . . . . 115
5.2.5 Desenvolvendo o código que disponibiliza o
objeto . . . . . . . . . . . . . . . . .. . . . 115
5.2.6 Escrevendo o cliente . . . . . . . . . . . . . 116
5.3 Testando tudo . . . . . . . . . . . . . . . . . . . . . 117
5.3.1 No Windows . . . . . . . . . . . . . . . . . 117
5.3.2 No Linux . . . . . . . . . . . . . . . . . . . 118
6 CORBA 119
6.1 O que é CORBA? . . . . . . . . . . . . . . . . . . 119
6.2 Exemplo CORBA em Java . . . . . . . . . . . . . . 126
6.2.1 Escrevendo a IDL . . . . . . . . . . . . . . 126
6.2.2 Compilando a IDL . . . . . . . . . . . . . . 127
6.2.3 Implementando nosso Objeto . . . . . . . . 127
6.2.4 Escrevendo o servidor . . . . . . . . . . . . 128
6.2.5 Escrevendo o cliente . . . . . . . . . . . . . 129
6.2.6 Rodando o exemplo . . . . . . . . . . . . . 130
6.3 Exemplo CORBA (Java + C) . . . . . . . . . . . . 130
6.3.1 Compilando a IDL . . . . . . . . . . . . . . 131
6.3.2 Implementando nosso Objeto . . . . . . . . 132
6.3.3 Escrevendo o Servidor . . . . . . . . . . . . 132
6.3.4 Escrevendo o Cliente . . . . . . . . . . . . . 134
6.3.5 Compilando e Rodando o Exemplo . . . . . 135
7 Servlets e JSP 137
7.1 Servlets . . . . . . . . . . . . . . . . . . . . . . . . 137
7.1.1 Applets X Servlets . . . . . . . . . . . . . . 139
7.1.2 CGI X Servlets . . . . . . . . . . . . . . . . 139
7.2 A API Servlet . . . . . . . . . . . . . . . . . . . . . 139
7.2.1 Exemplo de Servlet . . . . . . . . . . . . . . 142
7.3 Compilando o Servlet . . . . . . . . . . . . . . . . 143
7.3.1 Instalando o Tomcat . . . . . . . . . . . . . 143
7.4 Preparando para executar o Servlet . . . . . . . . . 148
7.4.1 Compilando o Servlet . . . . . . . . . . . . 148
7.4.2 Criando uma aplicação no Tomcat . . . . . 148
7.5 Executando o Servlet . . . . . . . . . . . . . . . . . 149
7.5.1 Invocando diretamente pelo Navegador . . . 149
7.5.2 Invocando em uma página HTML . . . . . 150
7.5.3 Diferenças entre as requisições GET e POST 150
7.6 Concorrência . . . . . . . . . . . . . . . . . . . . . 151
7.7 Obtendo Informações sobre a Requisição . . . . . 154
7.8 Lidando com Formulários . . . . . . . . . . . . . . 156
7.9 Lidando com Cookies . . . . . . . . . . . . . . . . . 157
7.10 Lidando com Sessões . . . . . . . . . . . . . . . . . 161
7.11 JSP . . . . . . . . . . . . . . . . . . . . . . . . . . 164
7.11.1 PHP X JSP . . . . . . . . . . . . . . . . . . 166
7.11.2 ASP X JSP . . . . . . . . . . . . . . . . . . 166
7.11.3 Primeiro exemplo em JSP . . . . . . . . . . 167
7.11.4 Executando o arquivo JSP . . . . . . . . . . 168
7.11.5 Objetos implícitos . . . . . . . . . . . . . . 169
7.11.6 Tags JSP . . . . . . . . . . . . . . . . . . . 169
7.11.7 Extraindo Valores de Formulários . . . . . . 175
7.11.8 Criando e Modificando Cookies . . . . . . . 177
7.11.9 Lidando com sessões . . . . . . . . . . . . . 178
7.11.10O Uso de JavaBeans . . . . . . . . . . . . . 180
7.11.11Escopo do bean . . . . . . . . . . . . . . . . 183
7.12 Reencaminhando ou Redirecionando requisições . . 187
7.13 Uma Arquitetura para comércio eletrônico . . . . . 189
7.13.1 Tipos de aplicações na WEB . . . . . . . . 189
7.13.2 Arquitetura MVC para a Web . . . . . . . 190
7.13.3 Agenda Web: Um Exemplo de uma aplica-
ção Web usando a arquitetura MVC . . . . 192
Capítulo 1
Introdução
Java é uma linguagem de programação desenvolvida pela Sun Mi-
crosystems e lançada em versão beta em 1995. O seu desenvolvi-
mento foi iniciado em 1991 pela equipe liderada por James Gos-
ling visando o mercado de bens eletrônicos de consumo. Por isso
foi projetada desde o início para ser independente de hardware,
uma vez que as características dos equipamentos variam ampla-
mente neste nicho de desenvolvimento. Outro objetivo estabele-
cido desde sua concepção foi o de ser uma linguagem segura. Se-
gura tanto no sentido de evitar algumas falhas comuns que os pro-
gramadores costumam cometer durante o desenvolvimento, como
no sentido de evitar ataques externos. Isto é importante no mer-
cado de bens eletrônicos de consumo porque ninguém gostaria de
adquirir um produto que necessitasse desligar e religar para que
voltasse a funcionar corretamente. Estas características desper-
taram o interesse para utilização de Java em outro ambiente que
também necessitava de uma linguagem com este perfil: a Internet.
A Internet também é um ambiente constituído por equipamen-
tos de diferentes arquiteturas e necessita muito de uma linguagem
que permita a construção de aplicativos seguros. Muitas pessoas
argumentarão que estas características podem ser encontradas em
outras linguagens e portanto isto não explica o súbito sucesso da
linguagem. Podemos arriscar alguns palpites apesar de este ser
um terreno um pouco pantanoso para se aventurar, até porque
1
2 Java na prática
as linguagens de programação tendem assumir um caráter quase
religioso. Uma das razões que na nossa opinião favoreceram a
rápida adoção da linguagem foi a sintaxe. Java é sintaticamente
muito semelhante à linguagem C/C++, apesar de existirem dife-
renças fundamentais na filosofia de implementação entre as duas
linguagens. Isto facilitou a migração de uma legião imensa de
programadores C/C++ para a nova linguagem. Outra razão que
não pode ser desprezada é o momento atual onde os desenvolve-
dores estão ansiosos para se libertarem de sistemas proprietários.
Portanto, apesar de não serem novas as idéias embutidas na lin-
guagem Java, a reunião delas em uma só linguagem, juntamente
com a facilidade de migração dos programadores e o momento
atual, contribuíram para o rápido sucesso da linguagem.
Hoje, segundo a International Data Corp. (IDC), existem mais
de 2 milhões de programadores Java no mundo e a estimativa
é que o número de desenvolvedores ultrapasse os 5 milhões em
2004. Segunda a Gartner, 62% das grandes companhias do Brasil
possuem algum tipo de aplicação em Java e em 2005 é previsto
que este número chegará a 80%. Os profissionais que dominam a
linguagem estão entre os mais bem pagos da área de Tecnologia
da Informação (TI), segundo a revista Info Exame (dezembro de
2001). A lista abaixo apresenta as principais características de
Java, de modo que o leitor tenha uma visão geral da linguagem:
• Orientação a objetos. Java não é uma linguagem to-
talmente orientada a objetos como Smalltalk, onde tudo é
objeto ou métodos de objetos. Por questões de eficiência fo-
ram mantidos alguns tipos primitivos e suas operações. No
entanto Java possui um grau de orientação a objetos bem
maior que C/C++, o que a torna bem mais harmoniosa e
fácil de assimilar, uma vez que o programador tenha com-
preendido esta forma de desenvolvimento.
• Compilação do código fonte para código de uma má-
quina virtual (Bytecodes). Esta característica visa tor-
nar a linguagem independente de plataforma de Hardware
e Sistema Operacional. Obviamente é necessário que exista
um programa capaz de interpretar o código em Bytecodes
para cada Sistema Operacional, denominado de Máquina
Tópicos Avançados 3
Virtual. Porém, nada impede que o código fonte seja tra-
duzido diretamente para o código executável na máquina
de destino. Já existem ambientes de desenvolvimento que
apresentam este tipo de opção. Alternativamente é possível
projetar equipamentos que processem em hardware os Byte-
codes. O diagrama da figura 1.1 ilustra as etapas envolvidas
na execução de um código Java.
Figura 1.1 Fases para execução de um programa fonte em
Java.
• Ausência de manipulação explícita de ponteiros. Em
linguagens como C/C++ e Pascal existe o tipo ponteiro
como tipo primitivo da linguagem. A especificação origi-
nal de Pascal é restritiva no uso de ponteiros, permitindo
que sejam usados apenas para referenciar memória obtida
na área de alocação dinâmica (heap) e não permiteque o
programador examine o valor da variável do tipo ponteiro,
nem que realize operações aritméticas com ponteiros. Já
a linguagem C/C++ permite que o valor armazenado na
variável do tipo ponteiro faça referência a qualquer área de
memória, inclusive à área estática e automática (pilha), além
de permitir aritmética de ponteiros e o exame direto do va-
lor armazenado. A manipulação do tipo ponteiro exige uma
grande dose de atenção por parte do programador e mesmo
programadores experientes frequentemente cometem erros
no seu uso. Além disso, o uso de ponteiros é uma fonte
de insegurança na linguagem, uma vez que permite que o
usuário faça acesso a memória que pode pertencer a outros
processos, abrindo a possibilidade para desenvolvimento de
programas hostis ao sistema. A linguagem Java não possui
o tipo ponteiro. Isto não quer dizer que não seja possível
4 Java na prática
realizar alocação dinâmica de memória. Todo objeto criado
é alocado na área de heap (memória de alocação dinâmica),
mas o usuário não pode manipular a referência ao objeto
explicitamente.
• Recuperação automática de memória não utilizada
(Coleta de Lixo - Garbage Collection). Nas linguagens
onde existe alocação dinâmica de memória, o programador
é responsável pela liberação de memória previamente obtida
na área de alocação dinâmica e que não está sendo mais uti-
lizada. Se houver falhas na execução desta responsabilidade
ocorrerá o problema que é conhecido sob a denominação de
"vazamento de memória". Este problema faz com que, a
partir de certo ponto, o programa não consiga obter me-
mória para criação de novos objetos, apesar de existir área
que não está sendo mais usada mas que não foi devolvida
ao gerente de memória. Outro erro comum é a tentativa de
acesso a áreas de memória já liberadas. Todos os programa-
dores que trabalham com linguagens que permitem alocação
dinâmica conhecem bem estes problemas e sabem o quanto
é difícil implementar programas que não possuam estes ti-
pos de erros. A maior parte dos erros que ocorrem no uso
destas linguagens é devido a problemas na alocação/libera-
ção de memória. Visando o desenvolvimento de aplicações
robustas, livres deste tipo de falha, os projetistas de Java in-
corporaram um procedimento de coleta automática de lixo
à máquina virtual. Deste modo, os objetos que não estão
sendo mais usados são identificados pelo procedimento, que
libera a memória para ser utilizada na criação de novos ob-
jetos.
• Segurança. As pessoas costumam dizer que Java é uma lin-
guagem segura. Mas o que é uma linguagem de programação
segura? Segurança possui significados distintos para pessoas
diferentes. No caso da linguagem Java na versão 1.0, segu-
rança significa impedir que programas hostis possam causar
danos ao ambiente computacional ou busquem informações
sigilosas em computadores remotos para uso não autorizado.
Na versão 1.1, foi adicionada a capacidade de permitir a ve-
Tópicos Avançados 5
rificação da identidade dos programas (autenticação) e, na
versão 1.2, os dados que os programas enviam e recebem po-
dem ser criptografados por meio do uso de um pacote adici-
onal. Na versão 1.4, o pacote de criptografia JCE (JavaTM
Cryptography Extension) foi incorporado ao J2SDK.
• Suporte à Concorrência. A construção de servidores,
a criação de programas com interfaces gráficas, e progra-
mas semelhantes que tem em comum a necessidade de que o
atendimento de uma solicitação não incapacite o sistema de
responder a outras solicitações concorrentemente, deman-
dam o uso de uma linguagem que facilite o desenvolvimento
deste tipo de programa. As linguagens projetadas antes do
surgimento destas necessidades, como C/C++, não previam
facilidades para este tipo de programação, o que obrigou a
incorporação destes recursos posteriormente, por meio de
funções adicionais. Como a programação concorrente é uma
forma de programação que difere bastante da programação
sequencial convencional, a simples adição de novas funções,
para tentar adaptar a linguagem a esta forma de codifica-
ção, não cria um ajuste perfeito com a linguagem subja-
cente. Por outro lado, Java foi projetada visando facilitar
a programação concorrente. Isto faz com que a criação de
linhas de execução (threads) seja bem mais natural dos que
nas linguagens tradicionais.Programação em rede. Java pos-
sui em seu núcleo básico classes para comunicação em rede
por meio dos protocolos pertencentes à pilha de protocolos
TCP/IP. A pilha de protocolos TCP/IP é a utilizada pela
Internet e tornou-se o padrão de fato para comunicação en-
tre computadores em uma rede heterogênea. Isto torna Java
particularmente atrativa para o desenvolvimento de aplica-
ções na Internet. Além disso Java está incorporando um am-
plo conjunto de soluções para computação distribuída, como
CORBA (Common Object Request Broker Architecture),
RMI (Remote Method Invocation) e Servlets/JSP (aplica-
ções Java que são executadas por servidores Web).
Após o lançamento da versão beta da linguagem em 1995, a
Sun tem liberado diversas evoluções da linguagem na forma de
6 Java na prática
versões e releases de um conjunto de ferramentas denominado de
Java Development Kit (JDK) até a versão 1.2, quando se passou
a denominar Java 2 SDK (Standard Development Kit). Isto ocor-
reu porque outros kits de desenvolvimento com propósitos espe-
cíficos foram lançados, como o J2EE (Java 2 Enterprise Edition),
voltado para aplicações distribuídas escaláveis e o J2ME (Java 2
Micro Edition), voltado para aplicações embutidas em dispositi-
vos eletrônicos (Celulares, handheld, etc.). Durante a elaboração
deste livro, a última versão estável do SDK era a de número 1.4
que pode ser obtida gratuitamente no site http://java.sun.com/.
1.1 Convenções
As seguintes convenções são usadas neste livro.
1. Fontes com larguras constantes são usadas em:
• exemplos de código
public class Ponto
{
private int x , y ;
}
• nomes de métodos, classes e variáveis mencionadas no
texto.
2. Fontes com larguras constantes em negrito são usadas dentro
de exemplos de códigos para destacar palavras chave.
3. Fontes em itálico são usadas:
• em termos estrangeiros;
• na primeira vez que for usado um termo cujo significado
não for conhecimento generalizado.
Capítulo 2
Concorrência
Um sistema operacional é dito concorrente se permite que mais de
uma tarefa seja executada ao mesmo tempo. Na prática a concor-
rência real ou paralelismo só é possível se o hardware subjacente
possui mais de um processador. No entanto, mesmo em computa-
dores com apenas um processador é possível obter um certo tipo
de concorrência fazendo com que o processador central execute
um pouco de cada tarefa por vez, dando a impressão de que as
tarefas estão sendo executadas simultaneamente.
Dentro da nomenclatura empregada, uma instância de um pro-
grama em execução é chamada de processo. Um processo ocupa
um espaço em memória principal para o código e para as variáveis
transientes (variáveis que são eliminadas ao término do processo).
Cada processo possui pelo menos uma linha de execução (Thread).
Para ilustrarmos o que é uma linha de execução suponha um de-
terminado programa prog1. Ao ser posto em execução é criado
um processo, digamos A, com uma área de código e uma área de
dados e é iniciada a execução do processo a partir do ponto de
entrada. A instrução inicial assim como as instruções subsequen-
tes formam uma linha de execução do processo A. Portanto, um
thread nada mais é que uma sequência de instruções que está em
execução de acordo com que foi determinado pelo programa. O
estado corrente da linha de execução é representada pela instru-
ção que estásendo executada. A figura 2.1 mostra a relação entre
7
8 Java na prática
estes elementos.
Figura 2.1 Relação entre Programa, Processo e Thread.
É possível existir mais de uma linha de execução em um único
processo. Cada linha de execução pode também ser vista como um
processo, com a diferença que enquanto cada processo possui sua
área de código e dados separada de outros processos, os threads
em um mesmo processo compartilham o código e a área de dados.
O que distingue um thread de outro em um mesmo processo é a
instrução corrente e uma área de pilha usada para armazenar o
contexto da sequência de chamadas de cada thread. Por isso os
threads também são chamados de processos leves (light process).
A figura 2.2 mostra esquematicamente a diferença entre processos
e threads.
Figura 2.2 (a) Processos; (b) Threads.
Tópicos Avançados 9
Sistemas monotarefas e monothreads como o MS-DOS pos-
suem apenas um processo em execução em um determinado ins-
tante e apenas um thread no processo. Sistemas multitarefas e
monothreads como o Windows 3.1 permitem vários processos em
execução e apenas um thread por processo. Sistemas multitare-
fas e multithread como o Solaris, OS/2, Linux, QNX e Windows
98/NT/XP/2000 permitem vários processos em execução e vários
threads por processo.
Como os threads em um mesmo processo possuem uma área de
dados em comum, surge a necessidade de controlar o acesso a essa
área de dados, de modo que cada thread não leia ou altere dados no
momento que estão sendo alterados por outro thread. A inclusão
de instruções para controlar o acesso a áreas compartilhadas torna
o código mais complexo do que o código de processosmonothreads.
Uma pergunta pode surgir na mente do leitor: se a inclusão
de mais de um thread torna o código mais complexo, então por-
que razão alguém projetaria código multithread? A resposta é:
processos com vários threads podem realizar mais de uma tarefa
simultaneamente e, por isso, são úteis na criação de processos
servidores, criação de animações e no projeto de interfaces com
o usuário que não ficam travadas durante a execução de alguma
função. Por exemplo, imagine um processo servidor a espera de
requisições de serviços. Podemos projetá-lo de modo que, ao sur-
gir uma solicitação de um serviço por um processo cliente, ele crie
um thread para atender a solicitação enquanto volta a esperar a
requisição de novos serviços. Com isto os processos clientes não
precisam esperar o término do atendimento de alguma solicitação
para ter sua requisição atendida.
O mesmo pode ser dito em relação ao projeto de interfaces com
o usuário. O processo pode criar threads para executar as funções
solicitadas pelo usuário, enquanto aguarda novas interações. Caso
contrário, a interface ficaria impedida de receber novas solicitações
enquanto processa a solicitação corrente, o que poderia causar
uma sensação de travamento ao usuário.
Outra aplicação para processos multithread é a animação de
interfaces. Nesse caso cria-se um ou mais threads para gerenciar
as animações enquanto outros threads cuidam das outras tarefas,
como por exemplo, a entrada de dados.
10 Java na prática
A rigor todas as aplicações acima como outras aplicações de
processosmultithread podem ser executados por meio de processos
monothreads. No entanto, o tempo gasto na mudança de contexto
1
entre processos na maioria dos sistemas operacionais é muito mais
lenta que a simples alternância entre threads, uma vez que a maior
parte das informações contextuais são compartilhadas pelos thre-
ads de um mesmo processo.
Mesmo que você não crie mais de um thread, todo processo
Java possui vários threads: thread para garbage collection, thread
para monitoramento de eventos, thread para carga de imagens,
etc.
2.1 Criando threads em Java
Processo Multithread não é uma invenção da linguagem Java. É
possível criar processos multithread em quase todas as linguagens
do mercado, como C++ e Object Pascal. No entanto, Java foi
projetada para trabalhar com threads e incorporou threads ao nú-
cleo básico da linguagem tornando, desta forma, mais natural o
seu uso. Na verdade o uso de threads está tão intimamente ligado
a Java que é quase impossível escrever um programa útil que não
seja multithread.
A classe Thread agrupa os recursos necessários para a criação
de um thread. A forma mais simples de se criar um thread é criar
uma classe derivada da classe Thread. Por exemplo:
class MeuThread extends Thread
{
. . .
}
É preciso também sobrescrever o método run() da classe
Thread. O método run() é o ponto de entrada do thread, da
1
Mudança de Contexto (task switch): é o conjunto de operações necessá-
rias para gravar o estado atual do processo corrente e recuperar o estado de
outro processo de modo a torná-lo o processo corrente.
Tópicos Avançados 11
mesma forma que o método main() é ponto de entrada de uma
aplicação. O exemplo 2.1 mostra uma classe completa.
public class MeuThread extends Thread
{
St r ing s ;
public MeuThread ( St r ing as )
{
super ( ) ;
s = new St r ing ( as ) ;
}
public void run ( )
{
for ( int i = 0 ; i < 5 ; i++)
System . out . p r i n t l n ( i+" "+s ) ;
System . out . p r i n t l n ( "FIM ! "+s ) ;
}
}
Exemplo 2.1 Subclasse da classe Thread.
No exemplo 2.1, o método run() contém o código que será
executado pelo thread. Ele possui um comando for que imprime
cinco vezes o atributo s. Para iniciar a execução de um thread cria-
se um objeto da classe e invoca-se o método start() do objeto.
O método start() cria o thread e inicia sua execução pelo mé-
todo run(). Se o método run() for chamado diretamente, então
nenhum thread novo será criado e o método run() será executado
no thread corrente. O exemplo 2.2 mostra uma forma de se criar
um thread usando a classe definida no exemplo 2.1.
public class TesteThread1
{
public stat ic void main ( St r ing [ ] a rgs )
{
new MeuThread ( "Linha1" ) . s t a r t ( ) ;
}
}
Exemplo 2.2 Criação de um Thread.
12 Java na prática
No exemplo anterior apenas um thread, além do principal é cri-
ado. Nada impede que sejam criados mais objetos da mesma classe
para disparar um número maior de threads. O exemplo 2.3 mos-
tra a execução de dois threads sobre dois objetos de uma mesma
classe.
public class TesteThread2
{
public stat ic void main ( St r ing [ ] a rgs )
{
new MeuThread ( "Linha1" ) . s t a r t ( ) ;
new MeuThread ( "Linha2" ) . s t a r t ( ) ;
}
}
Exemplo 2.3 Criação de dois threads.
Cada thread é executado sobre uma instância da classe e, por
consequência, sobre uma instância do método run(). A saída ge-
rada pela execução do exemplo 2.3 depende do sistema operacional
subjacente. Uma saída possível é a seguinte:
0 Linha2
0 Linha1
1 Linha2
1 Linha1
2 Linha2
2 Linha1
3 Linha2
3 Linha1
4 Linha2
4 Linha1
FIM! Linha2
FIM! Linha1
Esta saída mostra que os threads executam intercaladamente.
No entanto, em alguns sistemas operacionais os threads do exem-
plo 2.3 executariam um após o outro. A relação entre a sequência
de execução e o sistema operacional e dicas de como escrever pro-
gramas multithread com sequência de execução independente de
plataforma operacional serão nas seções 2.5 e 2.6.
Tópicos Avançados 13
2.1.1 Usando a interface Runnable
Algumas vezes não é possível criar uma subclasse da classe Thread
porque a classe já deriva outra classe, por exemplo a classe Applet.
Outras vezes, por questões de pureza de projeto, o projetista não
deseja derivar a classe Thread simplesmente para poder criar um
thread, uma vez que isto viola o significado da relação de classe-
subclasse.Para esses casos existe a interface Runnable. A in-
terface Runnable possui apenas um método para ser implemen-
tado: o método run(). Para criar um thread usando a interface
Runnable é preciso criar um objeto da classe Thread, passando
para o construtor uma instância da classe que implementa a inter-
face. Ao invocar o método start() do objeto da classe Thread,
o thread criado inicia sua execução no método run() da instância
da classe que implementou a interface. O exemplo 2.4 mostra a
criação de um thread usando a interface Runnable.
public class TesteThread2 implements Runnable
{
private St r ing men ;
public stat ic void main ( St r ing args [ ] )
{
TesteThread2 ob1 = new TesteThread2 ( " o la " ) ;
Thread t1 = new Thread ( ob1 ) ;
t1 . s t a r t ( ) ;
}
public TesteThread2 ( St r ing men) { this .men=men ; }
public void run ( )
{
for ( ; ; ) System . out . p r i n t l n (men ) ;
}
}
Exemplo 2.4 Criação de um thread por meio da interface
Runnable.
Note que agora ao invocarmos o método start() o thread
criado iniciará a execução sobre o método run() do objeto passado
como parâmetro, e não sobre o método run() do objeto Thread.
Nada impede que seja criado mais de um thread executando sobre
14 Java na prática
o mesmo objeto:
Thread t1 = new Thread ( ob1 ) ;
Thread t2 = new Thread ( ob1 ) ;
Neste caso alguns cuidados devem ser tomados, uma vez que
existe o compartilhamento das variáveis do objeto por dois threads.
Os problemas que podem advir de uma situação como esta serão
tratados mais adiante.
2.2 A classe Thread
A classe Thread é extensa, possuindo vários construtores, métodos
e variáveis públicas. Aqui mostraremos apenas os mais usados.
Hierarquia
A classe Thread deriva diretamente da classe Object.
java.lang.Object
|
+--java.lang.Thread
Construtores
A tabela 2.1 mostra os principais construtores da classe Thread.
Podemos notar que é possível nomear os threads e agrupá-los. Isto
é útil para obter a referência de threads por meio do seu nome.
Tópicos Avançados 15
Construtor Descrição
Thread(ThreadGroup g, String nome)
Cria um novo thread com o
nome especificado dentro do
grupo g.
Thread(Runnable ob, String nome)
Cria um novo thread para exe-
cutar sobre o objeto ob, com o
nome especificado.
Thread(ThreadGroup g,
Runnable ob, String nome)
Cria um novo thread para exe-
cutar sobre o objeto ob, dentro
do grupo g, com o nome espe-
cificado.
Thread(String nome)
Cria um novo thread com o
nome especificado.
Thread()
Cria um novo thread com o
nome default.
Thread(Runnable ob)
Cria um novo thread para exe-
cutar sobre o objeto ob.
Thread(ThreadGroup g, Runnable ob)
Cria um novo thread para exe-
cutar sobre o objeto ob, dentro
do grupo g.
Tabela 2.1 Principais construtores da classe Thread.
Métodos
A tabela 2.2 apresenta os principais métodos da classe Thread.
Alguns métodos muito usados nas versões anteriores do SDK1.2
estão sendo descontinuados (deprecated) por serem considerados
inseguros ou com tendência a causarem deadlock
2
. Os métodos
descontinuados são: stop(), suspend() e resume().
2
Travamento causado pela espera circular de recursos em um conjunto de
threads. O travamento por deadlock mais simples é o abraço mortal onde um
thread A espera que um thread B libere um recurso, enquanto que o thread
B só libera o recurso esperado por A se obter um recurso mantido por A.
Desta forma os dois threads são impedidos indefinidamente de prosseguir.
16 Java na prática
Método Descrição
static Thread currentThread()
Retorna uma referência para o
thread corrente em execução.
static int enumerate(Thread[] v)
Copia para o array todos os th-
read ativos no grupo do thread.
String getName()
Obtém o nome do thread.
int getPriority()
Obtém a prioridade do thread.
ThreadGroup getThreadGroup()
Retorna o grupo do thread.
void interrupt()
Interrompe este thread.
void run()
Se o thread foi construído
usando um objeto Runnable
separado então o método do
objeto Runnable é chamado.
Caso contrário nada ocorre.
void setName(String name)
Muda o nome do thread.
void setPriority(int p)
Muda a prioridade do thread.
static void sleep(long milis)
Suspende o thread em execução
o número de milissegundos es-
pecificados.
static void sleep(long milis,
int nanos)
Suspende o thread em execu-
ção o número de milissegundos
mais o número de nanossegun-
dos especificados.
void start()
Inicia a execução do thread. A
máquina virtual chama o mé-
todo run() do thread.
static void yield()
Faz com que o thread corrente
interrompa permitindo que ou-
tro thread seja executado.
Tabela 2.2 Principais métodos da classe Thread.
Existem alguns métodos da classe Object que são importantes
para o controle dos threads, como mostra a tabela 2.3. O leitor
pode estar se perguntando porque métodos relacionados a threads
estão na superclasse Object que é �mãe� de todas as classe em
Java. A razão disso é que esses métodos lidam com um elemento
associado a todo objeto e que é usado para promover o acesso
exclusivo aos objetos. Esse elemento é chamado de monitor. Na
seção 2.6 os monitores serão discutidos mais detalhadamente.
Tópicos Avançados 17
Método Descrição
void notify()
Notifica um thread que está espe-
rando sobre um objeto.
void notifyAll()
Notifica todos os threads que estão
esperando sobre um objeto.
void wait()
Espera para ser notificado por ou-
tro thread.
void wait(long milis,
int nanos)
Espera para ser notificado por ou-
tro thread ou até que se passe o
tempo em milissegundos expresso
por milis, adicionado ao tempo
em nanossegundos expresso por
nanos.
void wait(long milis)
Espera para ser notificado por ou-
tro thread ou até que se passe o
tempo em milissegundos expresso
por milis.
Tabela 2.3 Métodos da classe Object relacionados com
threads.
2.2.1 Variáveis públicas
As variáveis públicas da classe Thread definem valores máximo,
mínimo e default para a prioridade de execução dos threads. Java
estabelece dez valores de prioridade. Como essas prioridades são
associadas às prioridades do ambiente operacional então, cada
implementação de máquina virtual pode ter uma associação di-
ferente, o que pode influenciar no resultado final da execução do
programa. Na seção 2.5 abordaremos a influência do ambiente
operacional na execução de programas multithread.
Método Descrição
static final int MAX_PRIORITY
A prioridade máxima que um th-
read pode ter.
static final int MIN_PRIORITY
A prioridade mínima que um th-
read pode ter.
static final int NORM_PRIORITY
A prioridade default associado a
um thread.
Tabela 2.4 Variáveis públicas.
18 Java na prática
2.3 Ciclo de Vida dos Threads
Um thread pode possuir quatro estados conforme mostra a fi-
gura 2.3. Podemos observar que uma vez ativo o thread alterna
os estados em execução, suspenso e pronto até que passe para
o estado morto. A transição de um estado para outro pode ser
determinada por uma chamada explícita a um método ou devida
a ocorrência de algum evento no nível de ambiente operacional ou
de programa.
Figura 2.3 Estados de um thread.
A transição de um thread do estado novo para algum estado
ativo é sempre realizada pela invocação do método start() do
objeto Thread. Já as transições do estado em execução para o
estado suspenso, do suspenso para o estado pronto e desses
para o estado morto podem ser disparadas tanto pela invocação
de variados métodos como pela ocorrência de eventos. O exem-
plo 2.5 mostra as ocorrências de transição em um código.
public class TesteThread3 extends Thread{
public TesteThread3 ( S t r ing s t r ) { super ( s t r ) ; }
public void run ( )
{
for ( int i = 0 ; i < 10 ; i++)
{
System . out . p r i n t l n ( i + " " + getName ( ) ) ;
try {
// Comando para suspender o thread por
Tópicos Avançados 19
// 1000 mi l i segundos ( 1 segundo )
// Transição do es tado em execução
// para o es tado suspenso
s l e e p ( 1000 ) ;
} catch ( Inter ruptedExcept ion e ) {}
// Evento : fim do tempo de suspensão
// Transição do es tado em suspenso
// para o es tado pronto e de s t e p/execução
}
System . out . p r i n t l n ( "FIM ! " + getName ( ) ) ;
// Evento : fim da execução do thread
// Transição do es tado a t i v o suspenso para o
// es tado morto
}
public stat ic void main ( St r ing args [ ] )
{
TesteThread3 t1 = new TesteThread3 ( args [ 0 ] ) ;
t1 . s t a r t ( ) ; // Transição para um estado a t i v o
}
}
Exemplo 2.5 Alguns comandos e eventos que acarretam
transição de estados.
2.3.1 sleep(), yield(), join(), stop(), suspend()
e resume()
Agora que vimos os estados que podem ser assumidos por um
thread em seu ciclo de vida vamos examinar mais detalhadamente
alguns dos métodos responsáveis pela mudança de estado de um
thread.
sleep
O método sleep() é um método estático e possui as seguintes
interfaces:
static void sleep(long ms)
throws InterruptedException
ou
20 Java na prática
static void sleep(long ms, int ns)
throws InterruptedException
onde ms é um valor em milissegundos e ns é um valor em nanos-
segundos.
O método sleep() faz com que o thread seja suspenso
por um determinado tempo, permitindo que outros threads
sejam executados. Como o método pode lançar a exceção
InterruptedException, é preciso envolver a chamada em um
bloco try/catch ou propagar a exceção. O exemplo 2.6 define
uma espera mínima de 100 milisegundos entre cada volta da ite-
ração. Note que o tempo de suspensão do thread pode ser maior
que o especificado, uma vez que outros threads de maior ou mesmo
de igual prioridade podem estar sendo executados no momento em
que expira o tempo de suspensão solicitado.
public class ThreadComSleep extends Thread
{
St r ing s ;
public ThreadComSleep ( St r ing as )
{
super ( ) ;
s = new St r ing ( as ) ;
}
public void run ( )
{
for ( int i = 0 ; i < 5 ; i++)
{
System . out . p r i n t l n ( i+" "+s ) ;
try{
Thread . s l e e p ( 1 0 0 ) ;
catch ( Inter ruptedExcept ion e ){}
}
System . out . p r i n t l n ( "FIM ! "+s ) ;
}
}
Exemplo 2.6 Uso do método sleep().
Outro problema com o sleep() é que a maioria dos sistemas
operacionais não suportam resolução de nanossegundos. Mesmo a
Tópicos Avançados 21
resolução na unidade de milissegundo não é suportada em alguns
sistemas operacionais. No caso do sistema operacional não supor-
tar a resolução de tempo solicitada, o tempo será arredondado
para a nível de resolução suportado pela plataforma operacional.
yield
O método yield() é um método estático com a seguinte interface:
static void yield()
Uma chamada ao método yield() faz com que o thread cor-
rente libere automaticamente a CPU (Central Processing Unit)
para outro thread de mesma prioridade. Se não houver nenhum
outro thread de mesma prioridade aguardando, então o thread cor-
rente mantém a posse da CPU. O exemplo 2.7 altera o exemplo 2.1
de modo a permitir que outros threads de mesma prioridade sejam
executados a cada volta da iteração.
public class ThreadComYield extends Thread
{
St r ing s ;
public ThreadComYield ( S t r ing as )
{
super ( ) ;
s = new St r ing ( as ) ;
}
public void run ( )
{
for ( int i = 0 ; i < 5 ; i++)
{
System . out . p r i n t l n ( i+" "+s ) ;
Thread . y i e l d ( ) ;
}
System . out . p r i n t l n ( "FIM ! "+s ) ;
}
}
Exemplo 2.7 Uso do método yield().
22 Java na prática
join
O método join() é um método de instância da classe Thread e é
utilizado quando existe a necessidade do thread corrente esperar
pelo término da execução de outro thread. As versões do método
join() são as seguintes:
public final void join();
public final void join(long millisecond);
public final void join(long millisecond, int
nanosecond);
Na primeira versão o thread corrente espera indefinidamente
pelo encerramento da execução do segundo thread. Na segunda e
terceira versão o thread corrente espera pelo término da execução
do segundo thread até no máximo um período de tempo prefixado.
O exemplo 2.8 mostra como usar o método join().
class ThreadComJoin extends Thread
{
St r ing s ;
public ThreadComJoin ( St r ing as )
{
super ( ) ;
s = new St r ing ( as ) ;
}
public void run ( )
{
for ( int i = 0 ; i < 10 ; i++)
System . out . p r i n t l n ( i+" "+s ) ;
System . out . p r i n t l n ( "Fim do thread ! " ) ;
}
}
public c lass TestaJoin
{
public stat ic void main ( St r ing args [ ] )
{
ThreadComJoin t1 = new ThreadComJoin ( args [ 0 ] ) ;
t1 . s t a r t ( ) ; // Transição para um estado a t i vo
t1 . j o i n ( ) ; // Espera pe lo término do thread
System . out . p r i n t l n ( "Fim do programa ! " ) ;
}
}
Exemplo 2.8 Uso do método join().
Tópicos Avançados 23
stop, suspend e resume
A partir da versão 1.2 do SDK os métodos stop(), suspend() e
resume() tornaram-se deprecated (serão descontinuados) uma vez
que a utilização desses métodos tendia a gerar erros. No entanto,
devido a grande quantidade de código que ainda utiliza estes mé-
todos, acreditamos que seja importante mencioná-los.
O método stop() é um método de instância que encerra a
execução do thread ao qual pertence. Os recursos alocados ao
thread são liberados. É recomendável substituir o método stop()
pelo simples retorno do método run().
O método suspend() é um método de instância que suspende
a execução do thread ao qual pertence. Nenhum recurso é libe-
rado, inclusive os monitores que possua no momento da suspensão
(os monitores serão vistos na seção 2.6 e servem para controlar o
acesso à variáveis compartilhadas). Isto faz com que o método
suspend() tenda a ocasionar deadlocks.
O método resume() é um método de instância que reassume
a execução do thread ao qual pertence. Os métodos suspend() e
resume() devem ser substituídos respectivamente pelos métodos
wait() e notify(), como veremos na seção 2.6.
2.4 Daemon Threads
Daemon threads são threads que rodam em background com a
função de prover algum serviço mas não fazem parte do propósito
principal do programa. Quando só existem threads do tipo dae-
mon o programa é encerrado. Um exemplo de daemon é o thread
para coleta de lixo.
Um thread é definido como daemon por meio do método de
instância setDaemon(). Para verificar se um thread é um daemon
é usado o método de instância isDaemon(). O exemplo 2.9 mostra
como usar esses métodos.
import java . i o . ∗ ;
class ThreadDaemon extends Thread
{
public ThreadDaemon ( )
24 Java na prática
{
setDaemon ( true ) ;
s t a r t ( ) ;
}
public void run ( )
{
for ( ; ; ) y i e l d ( ) ;
}
}
public class TestaDaemon
{
public stat ic void main ( St r ing [ ] a rgs )
{
Thread d = new ThreadDaemon ( ) ;
System . out . p r i n t l n ( "d . isDaemon () = "+
d . isDaemon ( ) ) ;
BufferedReader s td in = new BufferedReader (
new InputStreamReader ( System . in ) ) ;
System . out . p r i n t l n ( " D ig i t e qualquer c o i s a " ) ;
try
{
s td in . readLine ( ) ;
} catch ( IOException e ) {}
}
}
Exemplo 2.9 Uso dos métodos relacionados com daemons.
No exemplo 2.9 o método main() da classe TestaDaemon
cria um objeto da classe ThreadDaemon. O construtor da classeThreadDaemon define o thread como daemon por meio do método
setDaemon() e inicia a execução do thread. Como é apenas um
thread de demonstração o método run() da classe ThreadDaemon
não faz nada, apenas liberando a posse da CPU toda vez que
a adquire. Após a criação da instância da classe ThreadDaemon
no método main() o objeto é testado para verificar se é um da-
emon, utilizando para esse fim o método isDaemon(). Depois
disso o programa simplesmente espera o usuário pressionar a te-
cla <enter>. O programa termina logo após o acionamento da
tecla, mostrando dessa forma que o programa permanece ativo
apenas enquanto existem threads não daemons ativos.
Tópicos Avançados 25
2.5 A Influência do Sistema Operacional
sobre os Threads
Apesar da linguagem Java prometer a construção de programas
independentes de plataforma operacional, o comportamento dos
threads pode ser fortemente influenciado pelo sistema operacional
subjacente. Portanto, o programador deve tomar alguns cuidados
se deseja construir programas que funcionem da mesma forma,
independente do ambiente onde serão executados.
Alguns sistemas operacionais não oferecem suporte a execução
de threads. Neste caso, cada processo possui apenas um thread.
Mesmo em sistemas operacionais que oferecem suporte a execução
de múltiplos threads por processo, o projetista da máquina virtual
pode optar por não usar o suporte nativo a threads. Deste modo,
é responsabilidade da máquina virtual criar um ambiente multith-
read. Threads implementados desta forma, no nível de usuário,
são chamados de green-threads. As influências da plataforma ope-
racional podem ser agrupadas em dois tipos:
1. Forma de escalonamento de threads. O ambiente pode
adotar um escalonamento não preemptivo ou preemptivo.
No escalonamento não preemptivo (também chamado de co-
operativo) um thread em execução só perde o controle da
CPU se a liberar voluntariamente ou se necessitar de algum
recurso que ainda não está disponível. Já no escalonamento
preemptivo, além das formas acima, um thread pode per-
der o controle da CPU por eventos externos, como o fim
do tempo máximo definido pelo ambiente para a execução
contínua de um thread (fatia de tempo) ou porque um th-
read de mais alta prioridade está pronto para ser executado.
Exemplos de sistemas operacionais não preemptivos são MS-
Windows 3.1 e IBM OS/2. Exemplos de sistemas operacio-
nais preemptivos são MS-Windows 95/98/2000/XP, Linux,
QNX e muitos outros. Alguns sistemas operacionais ado-
tam uma abordagem híbrida, suportando tanto o modelo
cooperativo como o preemptivo, como o Solaris da Sun.
2. Relacionamento entre os níveis de prioridades defi-
26 Java na prática
nidas na linguagem Java e os níveis de prioridades
definidas nos Sistemas Operacionais. Em um SO pre-
emptivo um thread de uma determinada prioridade perde a
posse da CPU para um thread de prioridade mais alta que
esteja pronto para ser executado. A linguagem Java prevê
dez níveis de prioridades que podem ser atribuídas aos th-
reads. No entanto, cada SO possui um número de priorida-
des diferente e o mapeamento das prioridades da linguagem
Java para as prioridades do SO subjacente pode influenciar
o comportamento do programa.
2.5.1 Forma de escalonamento de threads
A especificação da máquina virtual Java determina que a forma
de escalonamento de threads seja preemptiva. Portanto, mesmo
em ambientes operacionais cooperativos a máquina virtual deve
garantir um escalonamento preemptivo. No entanto, um esca-
lonamento preemptivo não obriga a preempção por fim de fatia
de tempo. Podemos ter um escalonamento preemptivo onde um
thread de mais alta prioridade interrompe o thread que detem
a posse da CPU mas não existe preempção por fim de fatia de
tempo. Um escalonamento onde threads de mesma prioridade in-
tercalam a posse da CPU por força do fim da fatia de tempo é
chamado de escalonamento Round-Robin. A especificação da má-
quina virtual Java não prevê o escalonamento Round-Robin, mas
também não o descarta, abrindo a possibilidade de implementa-
ções distintas de máquinas virtuais e introduzindo o não deter-
minismo na execução de programas multithread. O exemplo 2.3
poderia ter uma saída distinta da apresentada anteriormente caso
seja executado por uma máquina virtual que não implementa o
escalonamento Round-Robin. Nesse caso a saída seria a seguinte:
0 Linha2
1 Linha2
2 Linha2
3 Linha2
4 Linha2
FIM! Linha2
0 Linha1
Tópicos Avançados 27
1 Linha1
2 Linha1
3 Linha1
4 Linha1
FIM! Linha1
Neste caso, se o programador deseja que a execução de threads
se processe de forma alternada, independentemente da implemen-
tação da máquina virtual, então é necessário que ele insira código
para a liberação voluntária da CPU. Isso pode ser feito com o
método yield() ou com o método sleep().
2.5.2 Relacionamento entre os níveis de prio-
ridades definidas na linguagem Java e os
níveis de prioridades definidas nos Siste-
mas Operacionais
Como já dissemos a linguagem Java prevê dez níveis de priorida-
des que podem ser atribuídas aos threads. Na verdade são onze
prioridades, mas a prioridade de nível 0 é reservada para threads
internos. As prioridades atribuídas aos threads são estáticas, ou
seja não se alteram ao longo da vida do thread, a não ser que por
meio de chamadas a métodos definidos para esse propósito. A
classe thread possui variáveis públicas finais com valores de prio-
ridade predefinidos, como mostrado na tabela 2.4. No entanto, os
sistemas operacionais podem possuir um número maior ou menor
de níveis de prioridades. Vamos citar um exemplo: o MSWin-
dows 9x/NT. Este sistema possui apenas sete níveis de priorida-
des e estes sete níveis devem ser mapeados para os onze níveis
de prioridades especificados em Java. Cada máquina virtual fará
este mapeamento de modo diferente, porém a implementação mais
comum é mostrada na tabela 2.5.
Note que, nesta implementação, níveis de prioridades diferen-
tes em Java serão mapeados para um mesmo nível de prioridade
em MSWindows. Isto pode levar a resultados inesperados caso o
programador projete uma aplicação esperando, por exemplo, que
um thread de prioridade 4 irá interromper um thread de priori-
dade 3. Para evitar este tipo de problema o programador pode
28 Java na prática
adotar dois tipos de abordagem:
1. utilizar, se for possível, apenas as prioridades
Thread.MIN_PRIORITY, Thread.NORM_PRIORITY e
Thread.MAX_PRIORITY para atribuir prioridades aos
threads; ou
2. não se basear em níveis de prioridades para definir o escalo-
namento de threads, utilizando, alternativamente, primitivas
de sincronização que serão abordadas na próxima seção.
Prioridades Java Prioridades MSWindows
0 THREAD_PRIORITY_IDLE
1(Thread.MIN_PRIORITY) THREAD_PRIORITY_LOWEST
2 THREAD_PRIORITY_LOWEST
3 THREAD_PRIORITY_BELOW_NORMAL
4 THREAD_PRIORITY_BELOW_NORMAL
5(Thread.NORM_PRIORITY) THREAD_PRIORITY_NORMAL
6 THREAD_PRIORITY_ABOVE_NORMAL
7 THREAD_PRIORITY_ABOVE_NORMAL
8 THREAD_PRIORITY_BELOW_HIGHEST
9 THREAD_PRIORITY_BELOW_HIGHEST
10(Thread.MAX_PRIORITY) THREAD_TIME_CRITICAL
Tabela 2.5 Mapeamento das prioridades de Java para
MSWindows.
2.6 Compartilhamento de Memória e
Sincronização
Como já foi dito, mais de um thread pode ser criado sobre um
mesmo objeto. Neste caso, cuidados especiais devem ser tomados,
uma vez que os threads compartilham as mesmas variáveis e pro-
blemas podem surgir se um thread está atualizando uma variável
enquanto outro thread está lendo ou atualizando a mesma variá-
vel. Este problema pode ocorrer mesmo em threads que executam
sobre objetos distintos, já que os objetos podem possuir referên-
cias para um mesmo objeto. Oexemplo 2.10 mostra a execução de
Tópicos Avançados 29
dois threads sobre um mesmo objeto. O nome do thread é usado
para que o thread decida que ação tomar. O thread de nome �um�
cria um número de 0 a 1000 gerado aleatoriamente e o coloca na
posição inicial de um array de dez posições. As outras posições
do array são preenchidas com os nove números inteiros seguintes
ao número inicial. O thread de nome �dois� imprime o conteúdo
do vetor. A intenção inicial do projetista é obter na tela sequên-
cias de dez números inteiros consecutivos iniciados aleatoriamente.
No entanto, como os dois threads compartilham o mesmo objeto
e não existe qualquer sincronismo entre eles, é pouco provável que
o projetista obtenha o resultado esperado.
public class CalcDez implements Runnable
{
private int ve t In t [ ] ;
public CalcDez ( ) { ve t In t=new int [ 1 0 ] ; }
public void run ( )
{
i f
( Thread . currentThread ( ) . getName ( ) . equa l s ( "um" ) )
for ( ; ; )
{
ve t In t [ 0 ] = ( int ) (Math . random ( ) ∗ 1 0 0 0 ) ;
for ( int i =1; i <10; i++)
ve t In t [ i ]= ve t In t [0 ]+ i ;
}
else
for ( ; ; )
{
System . out . p r i n t l n ( " S e r i e i n i c i a d a por"+
vet In t [ 0 ] ) ;
for ( int i =1; i <10; i++)
System . out . p r i n t l n ( ve t In t [ i ]+ " " ) ;
}
}
public stat ic void main ( St r ing args [ ] )
{
CalcDez ob = new CalcDez ( ) ;
Thread t1 = new Thread (ob , "um" ) ;
Thread t2 = new Thread (ob , " do i s " ) ;
t1 . s t a r t ( ) ;
t2 . s t a r t ( ) ;
30 Java na prática
}
}
Exemplo 2.10 Dois threads executando sobre o mesmo objeto.
Se a máquina virtual não implementar um escalonamento
Round-Robin apenas um thread será executado, visto que os dois
threads possuem a mesma prioridade. Já no caso da máquina vir-
tual implementar um escalonamento Round-Robin a alternância
da execução dos threads produzirá resultados imprevisíveis. Um
trecho de uma das saídas possíveis pode ser visto na figura 2.4.
Ele foi obtido em Pentium 100MHz executando a máquina virtual
da Sun, versão 1.2, sob o sistema operacional MSWindows 95.
258
259
Serie iniciada por573
574
575
576
577
578
579
580
581
582
Serie iniciada por80
81
82
Figura 2.4 Saída do exemplo 2.10.
Podemos notar as sequências estão misturadas, mostrando que
cada thread interrompe o outro no meio da execução da tarefa
especificada. O mesmo problema pode ocorrer mesmo em threads
que executam sobre objetos diferentes, bastando que cada thread
possua uma referência para um mesmo objeto. O exemplo 2.11
mostra a execução de dois threads sobre objetos distintos.
Tópicos Avançados 31
class Compartilhada
{
private int ve t In t [ ] ;
public Compartilhada ( ) { ve t In t=new int [ 1 0 ] ; }
public void setVal ( )
{
for ( ; ; )
{
ve t In t [ 0 ] = ( int ) (Math . random ( ) ∗ 1 0 0 0 ) ;
for ( int i =1; i <10; i++)
vet In t [ i ]= ve t In t [0 ]+ i ;
}
}
public int getVal ( int i ) { return ve t In t [ i ] ; }
}
public c lass CalcDez2 extends Thread
{
private Compartilhada obj ;
private int t ipo ;
public CalcDez2 ( Compartilhada aObj , int aTipo )
{ obj = aObj ; t i po = aTipo ; }
public void run ( )
{
for ( ; ; )
i f ( t i po==1) obj . se tVal ( ) ;
else
{
System . out . p r i n t l n ( " S e r i e i n i c i a d a por"+
obj . getVal ( 0 ) ) ;
for ( int i =1; i <10; i++)
System . out . p r i n t l n ( obj . getVal ( i )+ " " ) ;
}
}
public stat ic void main ( St r ing args [ ] )
{
Compartilhada obj = new Compartilhada ( ) ;
CalcDez2 t1 = new CalcDez2 ( obj , 1 ) ;
CalcDez2 t2 = new CalcDez2 ( obj , 2 ) ;
t1 . s t a r t ( ) ;
t2 . s t a r t ( ) ;
}
}
Exemplo 2.11 Dois threads executando sobre objetos distintos.
É importante que o leitor não confunda o exemplo 2.11 com
o exemplo 2.10 achando que nos dois exemplos os dois threads
executam sobre o mesmo objeto, uma vez que a etapa da cria-
ção dos threads é bem parecida. No entanto, no exemplo 2.11
32 Java na prática
foi declarada uma subclasse da classe Thread e não uma classe
que implementa a interface Runnable. Apesar de parecer que no
exemplo 2.11 ambos os threads executam sobre um mesmo objeto
da classe Compartilhada que é passado como argumento, na ver-
dade cada thread executará sobre sua própria instância da classe
CalcDez2, sendo que o objeto da classe Compartilhada é referen-
ciado pelos dois threads. O comportamento do código do exem-
plo 2.11 é semelhante ao do exemplo 2.10, com a diferença que
no primeiro a sequência de inteiros é encapsulada pelo objeto da
classe Compartilhada.
Este tipo de situação, onde o resultado de uma computação
depende da forma como os threads são escalonados, é chamado
de condições de corrida (Race Conditions). É um problema a ser
evitado uma vez que o programa passa a ter um comportamento
não determinístico.
2.6.1 Atomicidade de Instruções e Sincroniza-
ção do Acesso à Sessões Críticas
A condição de corrida ocorre porque o acesso a áreas de memó-
ria compartilhadas é feito de forma não atômica, e de forma não
exclusiva. Por forma não atômica queremos dizer que o acesso é
feito por meio de várias instruções e pode ser interrompido por
outro thread antes que todas as instruções, que fazem parte do
acesso, sejam executadas. Por forma não exclusiva queremos di-
zer que um thread pode consultar/atualizar um objeto durante a
consulta/atualização do mesmo objeto por outros threads. Pou-
cas operações são atômicas em Java. Em geral, as atribuições
simples, com exceção dos tipos long e double, são atômicas, de
forma que o programador não precisa se preocupar em ser inter-
rompido no meio de uma operação de atribuição. No entanto, no
caso de operações mais complexas sobre variáveis compartilhadas
é preciso que o programador garanta o acesso exclusivo a essas
variáveis. Os trechos de código onde são feitos os acessos às variá-
veis compartilhadas são chamados de Seções Críticas ou Regiões
Críticas.
Uma vez determinada uma região crítica como garantir o
acesso exclusivo? A linguagem Java permite que o programador
Tópicos Avançados 33
garanta o acesso exclusivo utilizando o conceito de monitor . O
conceito de monitor foi proposto por C. A. R. Hoare em 1974 e
pode ser encarado como um objeto que garante a exclusão mútua
na execução dos procedimentos a ele associados. Ou seja, apenas
um procedimento associado ao monitor pode ser executado em um
determinado momento. Por exemplo, suponha que dois procedi-
mentos A e B estão associados a um monitor. Se no momento
da invocação do procedimento A o procedimento B estiver sendo
executado, então o processo ou thread que invocou o procedimento
A fica suspenso até o término da execução do procedimento B.
Ao término do procedimento B o processo que invocou o procedi-
mento A é �acordado� e sua execução retomada.
Figura 2.5 Uma possível sequência na disputa de dois threads
pela autorização de um monitor.
O uso de monitores em Java é uma variação do proposto por
34 Java na prática
Hoare. Na linguagem Java todo objeto possui um monitor asso-
ciado. Para facilitar o entendimento podemos encarar o monitor
como detentor de um �passe�. Todo thread pode pedir �empres-
tado� o passe ao monitor de um objeto antes de realizar alguma
computação. Como o monitor possui apenas um passe, apenas
um thread pode adquirir o passe em um determinado instante.
O passe tem que ser devolvido para o monitor para possibilitar
o empréstimo do passe a outro thread. A figura 2.5 ilustra essa
analogia.
Nos resta saber como solicitar o passe ao monitor. Isto é feito
por meio da palavra chave synchronized.Existem duas formas
de se usar a palavra chave synchronized: na declaração de mé-
todos e no início de blocos. O exemplo 2.12 mostra duas versões
da classe FilaCirc que implementa uma fila circular de valores
inteiros: uma com métodos synchronized e outra com blocos
synchronized. Um objeto desta classe pode ser compartilhado
por dois ou mais threads para implementar o exemplo clássico de
concorrência do tipo produtor/consumidor.
A palavra chave synchronized na frente dos métodos de ins-
tância significa que o método será executado se puder adquirir o
monitor do objeto a quem pertence o método
3
. Caso contrário, o
thread que invocou o método será suspenso até que possa adquirir
o monitor. Esta forma de sincronização é abordada no exem-
plo 2.12 a. Portanto, se algum thread chamar algum método de
um objeto da classe FilaCirc nenhum outro thread que compar-
tilha o mesmo objeto poderá executar um método do objeto até
que o método chamado pelo primeiro thread termine. Caso outro
thread invoque um método do mesmo objeto ficará bloqueado até
3
Não usaremos mais a analogia com a aquisição do passe do monitor. Ela
foi usada apenas para facilitar o entendimento do leitor. Quando se trata
de monitores os termos mais usados são: �adquirir o monitor� e �liberar o
monitor�.
Tópicos Avançados 35
que possa adquirir o monitor.
a) Versão com métodos synchronized
class FilaCirc
{
private final int TAM = 10;
private int vetInt[];
private int inicio, total;
public FilaCirc()
{
vetInt=new int[TAM];
inicio=0;
total =0;
}
public synchronized
void addElement(int v)
throws Exception
{
if (total == TAM) throw new
Exception("Fila cheia!");
vetInt[(inicio+total)%TAM] = v;
total++;
}
public synchronized
int getElement()
throws Exception
{
if (total == 0 ) throw new
Exception("Fila vazia!");
int temp = vetInt[inicio];
inicio = (++inicio)%TAM;
total--;
return temp;
}
}
b) Versão com blocos synchronized
class FilaCirc
{
private final int TAM = 10;
private int vetInt[];
private int inicio, total;
public FilaCirc()
{
vetInt=new int[TAM];
inicio=0;
total =0;
}
public void addElement(int v)
throws Exception
{
synchronized(this) {
if (total == TAM) throw new
Exception("Fila cheia!");
vetInt[(inicio+total)%TAM] = v;
total++;
}
}
public int getElement()
throws Exception
{
synchronized(this) {
if (total == 0 ) throw new
Exception("Fila vazia!");
int temp = vetInt[inicio];
inicio = (++inicio)%TAM;
total--;
}
return temp;
}
}
Exemplo 2.12 Duas versões de uma classe que implementa
uma fila circular de inteiros.
O leitor pode estar se perguntando sobre a necessidade de sin-
cronizar os métodos da classe FilaCirc uma vez que ocorrem
apenas atribuições simples a elementos individuais de um vetor e
as atribuições de inteiros são atômicas. De fato o problema ocorre
não na atribuição dos elementos e sim na indexação do array. Por
exemplo, a instrução
inicio = (++inicio)%TAM;
36 Java na prática
do método getElement() não é atômica. Suponha que os méto-
dos da classe FilaCirc não sejam sincronizados e que as variáveis
inicio e total possuam os valores 9 e 1 respectivamente. Su-
ponha também que thread invocou o método getElement() e foi
interrompido na linha de código mostrada acima após o incre-
mento da variável inicio mas antes da conclusão da linha de
código. Nesse caso o valor de inicio é 10. Se neste instante
outro thread executar o método getElement() do mesmo objeto
ocorrerá uma exceção IndexOutOfBoundsException ao atingir a
linha de código
int temp = vetInt[inicio];
se alterarmos a linha de código para
inicio = (inicio+1)%TAM;
evitaremos a exceção, mas não evitaremos o problema de retornar
mais de uma vez o mesmo elemento. Por exemplo, se um thread
for interrompido no mesmo local do caso anterior, outro thread
pode obter o mesmo elemento, uma vez que os valores de inicio
e total não foram alterados. Na verdade, o número de situa-
ções problemáticas, mesmo para esse exemplo pequeno, é enorme
e perderíamos muito tempo se tentássemos descrevê-las em sua
totalidade.
Em alguns casos pode ser indesejável sincronizar todo um mé-
todo, ou pode-se desejar adquirir o monitor de outro objeto, di-
ferente daquele a quem pertence o método. Isto pode ser feito
usando a palavra chave synchronized na frente de blocos. Esta
forma de sincronização é mostrada no exemplo 2.12 b. Neste
modo de usar a palavra-chave synchronized é necessário indicar
o objeto do qual se tentará adquirir o monitor. Caso o monitor
seja adquirido, o bloco é executado, caso contrário o thread é sus-
penso até que possa adquirir o monitor. O monitor é liberado no
final do bloco.
No exemplo 2.12 b, o monitor usado na sincronização é o
do próprio objeto do método, indicado pela palavra chave this.
Qualquer outro objeto referenciável no contexto poderia ser usado.
Tópicos Avançados 37
O que importa é que os grupos de threads, que possuam áreas de
código que necessitam de exclusão mútua, usem o mesmo objeto.
No exemplo 2.12 não existe vantagem da forma de implemen-
tação a) sobre a forma de implementação b) ou vice-versa. Isso
ocorre, principalmente, quando os métodos são muito pequenos
ou quando não realizam computações muito complexas. No en-
tanto, se o método for muito longo ou levar muito tempo para ser
executado, sincronizar todo o método pode �travar� em demasia a
execução da aplicação. Nesses casos, a sincronização somente das
seções críticas é mais indicada. Outra vantagem da segunda forma
de sincronização é a liberdade no uso de monitores de qualquer
objeto referenciável. Isto permite a implementação sincronizações
mais complexas como veremos mais adiante. O exemplo 2.13 mos-
tra como pode ser usado um objeto da classe FilaCirc.
public class TestaF i l aC i r c extends Thread
{
private Fi l aC i r c obj ;
private int t i po ;
public TestaF i l aC i r c ( F i l aC i r c aObj , int aTipo )
{ obj = aObj ; t i po = aTipo ; }
public void run ( )
{
for ( ; ; )
try
{
i f ( t i po==1)
{
int i = ( int ) (Math . random ( ) ∗ 1 0 0 0 ) ;
System . out . p r i n t l n ( "Elemento gerado : "+i ) ;
obj . addElement ( i ) ;
}
else System . out . p r i n t l n ( "Elemento obt ido : "+
obj . getElement ( ) ) ;
} catch ( Exception e )
{System . out . p r i n t l n ( e . getMessage ( ) ) ; }
}
public stat ic void main ( St r ing args [ ] )
{
F i l aC i r c obj = new Fi l aC i r c ( ) ;
Tes taF i l aC i r c t1 = new TestaF i l aC i r c ( obj , 1 ) ;
Tes taF i l aC i r c t2 = new TestaF i l aC i r c ( obj , 2 ) ;
38 Java na prática
t1 . s t a r t ( ) ;
t2 . s t a r t ( ) ;
}
}
Exemplo 2.13 Uso da fila circular de inteiros.
Um trecho possível da saída obtida na execução do programa
do exemplo 2.13 seria o seguinte:
...
Elemento obtido:154
Elemento gerado:725
Fila vazia!
Elemento gerado:801
Elemento obtido:725
Elemento gerado:204
Elemento obtido:801
...
É importante observar que o monitor em Java por si só não
implementa a exclusão mútua. Ele é apenas um recurso que pode
ser usado pelo programador para implementar o acesso exclusivo
à variáveis compartilhadas. Cabe ao programador a responsabili-
dade pelo uso adequado deste recurso. Por exemplo, se o progra-
mador esquecer de sincronizar um bloco ou método que necessite
de exclusão mútua, de nada adiantará ter sincronizado os outros
métodos ou blocos. O thread que executar o trecho não sincroni-
zado não tentará adquirir o monitor e, portanto, de nada adianta
outro thread te-lo o adquirido.
Outro ponto importante é usar a palavrachave synchronized
com muito cuidado. A sincronização custa muito caro em se tra-
tando de ciclos de CPU. A chamada de um método sincronizado é
por volta de 10 vezes mais lenta do que a chamada de um método
não sincronizado. Por essa razão use sempre a seguinte regra: não
sincronize o que não for preciso.
Tópicos Avançados 39
2.6.2 Comunicação entre Threads: wait() e no-
tify()
O exemplo 2.12 não é um modelo de uma boa implementação de
programa. O thread que adiciona elementos à fila tenta adicionar
um elemento a cada volta do laço de iteração, mesmo que a fila
esteja cheia. Por outro lado, o thread que retira os elementos da
fila tenta obter um elemento a cada volta do laço de iteração,
mesmo que a fila esteja vazia. Isto é um desperdício de tempo de
processador e pode tornar o programa bastante ineficiente.
Alguém poderia pensar em uma solução onde o thread testaria
se a condição desejada para o processamento ocorre. Caso a condi-
ção não ocorra o thread poderia executar o método sleep() para
ficar suspenso por algum tempo para depois testar novamente a
condição. O thread procederia desta forma até que a condição
fosse satisfeita. Este tipo de procedimento economizaria alguns
ciclos de CPU, evitando a tentativa incessante de se executar o
procedimento mesmo quando não há condições. O nome desta
forma de ação, onde o procedimento, a cada intervalo de tempo
pré-determinado testa se uma condição é satisfeita, é chamado de
espera ocupada (pooling ou busy wait).
No entanto, existem alguns problemas com este tipo de aborda-
gem. Primeiramente, apesar da economia de ciclos de CPU ainda
existe a possibilidade de ineficiência, principalmente se o tempo
não for bem ajustado. Se o tempo for muito curto ocorrerá vários
testes inúteis. Se for muito longo, o thread ficará suspenso além
do tempo necessário. Porém, mais grave que isto é que o método
sleep() não faz o thread liberar o monitor. Portanto, se o trecho
de código for uma região sincronizada, como é o caso do exem-
plo 2.12, de nada adiantará o thread ser suspenso. O thread que é
capaz de realizar a computação que satisfaz a condição esperada
pelo primeiro thread ficará impedido de entrar na região crítica,
ocorrendo assim um deadlock : o thread que detém o monitor es-
pera que a condição seja satisfeita e o thread que pode satisfazer a
condição não pode prosseguir porque não pode adquirir o monitor.
O que precisamos é um tipo de comunicação entre threads que
sinalize que certas condições foram satisfeitas. Além disso, é pre-
ciso que, ao esperar por determinada condição, o thread libere
40 Java na prática
o monitor. Esta forma de interação entre threads é obtida em
Java com o uso dos métodos de instância wait(), notify() e
notifyAll(). Como vimos anteriormente, esses métodos perten-
cem à classe Object e não à classe Thread. Isto ocorre porque
esses métodos atuam sobre os monitores, que são objetos relacio-
nados a cada instância de uma classe Java e não sobre os threads.
Ao invocar o método wait() de um objeto, o thread é suspenso
e inserido na fila do monitor do objeto, permanecendo na fila até
receber uma notificação. Cada monitor possui sua própria fila.
Ao invocar o método notify() de um objeto, um thread que está
na fila do monitor do objeto é notificado. Ao invocar o método
notifyAll() de um objeto, todos os threads que estão na fila do
monitor do objeto são notificados.
a)
class X
{
...
public synchronized
int mx()
{
...
// Espera uma condição
while(!cond) wait();
// Prossegue com a
// condição satisfeita
...
}
...
}
b)
class Y
{
X ob;
...
public int my()
{
...
synchronized (ob)
{
// Notifica algum
// thread
ob.notify();
...
}
...
}
c)
class Z
{
X ob;
...
public int mz()
{
...
synchronized (ob)
{
// Notifica todos
// os threads que
// esperam na fila
// do monitor de ob
ob.notifyAll();
...
}
...
}
Exemplo 2.14 Exemplos de chamadas dos métodos wait(),
notify() e notifyAll().
A única exigência é que esses métodos sejam invocados em um
thread que detenha a posse do monitor do objeto associado ao
método. Essa exigência faz sentido uma vez que eles sinalizam
a threads que esperam na fila desses monitores. Devido a essa
exigência, a invocação desses métodos ocorre em métodos ou blo-
cos sincronizados. O exemplo 2.14 mostra as formas mais comuns
de chamadas desses métodos. Note que o thread deve possuir o
Tópicos Avançados 41
monitor do objeto ao qual pertence o método. Por isso, nos exem-
plos 2.14 b e 2.14 c, o objeto sincronizado no bloco é o mesmo que
invoca os métodos notify() e notifyAll().
Outra observação importante é que o thread que invoca o mé-
todo wait() o faz dentro de um laço sobre a condição de espera.
Isto ocorre porque apesar de ter sido notificado isto não assegura
que a condição está satisfeita. O thread pode ter sido notificado
por outra razão ou, entre a notificação e a retomada da execução
do thread, a condição pode ter sido novamente alterada.
Uma vez notificado o thread não retoma imediatamente a exe-
cução. É preciso primeiro retomar a posse do monitor que no
momento da notificação pertence ao thread que notificou. Mesmo
após a liberação do monitor nada garante que o thread notificado
ganhe a posse do monitor. Outros threads podem ter solicitado a
posse do monitor e terem preferência na sua obtenção.
O exemplo 2.14 mostra apenas um esquema para uso dos mé-
todos para notificação. O exemplo 2.15 é uma versão do exem-
plo 2.12 a que usa os métodos de notificação para evitar problemas
como a espera ocupada. O exemplo 2.13 pode ser usado sem mo-
dificações para testar essa versão.
class Fi l aC i r c
{
private f ina l int TAM = 10;
private int ve t In t [ ] ;
private int i n i c i o , t o t a l ;
public Fi l aC i r c ( )
{
ve t In t=new int [TAM] ;
i n i c i o =0;
t o t a l =0;
}
public synchronized void addElement ( int v )
throws Exception
{
while ( t o t a l == TAM) wait ( ) ;
v e t In t [ ( i n i c i o+t o t a l )%TAM] = v ;
t o t a l++;
no t i f y ( ) ;
}
public synchronized int getElement ( )
42 Java na prática
throws Exception
{
while ( t o t a l == 0 ) wait ( ) ;
int temp = vet In t [ i n i c i o ] ;
i n i c i o = (++ i n i c i o )%TAM;
to ta l −−;
n o t i f y ( ) ;
return temp ;
}
}
Exemplo 2.15 Classe que implementa uma fila circular de
inteiros com notificação.
Figura 2.6 Uma possível sequência na execução de três threads.
A necessidade de se testar a condição em um comando de
repetição pode ser observada na figura 2.6 que mostra a evolu-
ção da execução de três threads sobre objetos que compartilham
uma instância da classe FilaCirc. O thread 3 executa o método
addElement(), no entanto, em virtude da condição total==TAM,
é obrigado a invocar o método wait() e esperar uma notifica-
ção. O próximo thread a assumir a CPU é o thread 1 que executa
o método getElement(), que estabelece a condição total<TAM
e executa um notify(). No entanto, o próximo thread a assu-
mir a CPU é o thread 2 e não o thread 3. O thread 2 executa o
Tópicos Avançados 43
método addElement(), o qual estabelece novamente a condição
total==TAM. Quando o thread 3 assume novamente a CPU, uma
vez que foi notificado, testa a condição e invoca novamente o mé-
todo wait() para esperar a condição favorável à execução. Caso
não testasse a condição em um comando de repetição o thread 3
tentaria inserir um elemento em uma fila cheia.
O método notify() não indica que evento ocorreu. No caso do
exemplo 2.15 existem dois tipos de eventos (a fila não está cheia
e a fila não está vazia), no entanto, podemos observar

Outros materiais

Materiais relacionados

Perguntas relacionadas

Perguntas Recentes