Baixe o app para aproveitar ainda mais
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
Compartilhar