Charles L. Perkins e Michael Morrison
Uma das características principais em Java programando o ambiente e o sistema em tempo de execução é a arquitetura multiroscada compartilhada por ambos. O multifilamento, que é um construto regularmente recente no mundo das Ciências da Computação, é um meio muito potente de aumento e controle de execução de programa. A lição de hoje dá uma olhada em como o multifilamento de suportes de língua de Java por meio do uso de fios. Aprenderá todos sobre as classes diferentes que permitem a Java ser uma língua roscada, junto com muitas das questões que rodeiam o uso efetivo de fios.
Para entender melhor a importância de fios, suponha que usa o seu editor de texto favorito em um grande arquivo. Quando lança, precisa de examinar o arquivo inteiro antes que o deixe comece a editar? Precisa de fazer uma cópia do arquivo? Se o arquivo for enorme, isto pode ser um pesadelo. Não seria mais bonito para ele mostrar-lhe a primeira página, permitindo-lhe começar a editar, e de qualquer maneira (em background) concluir as tarefas mais lentas necessárias para a inicialização? Os fios permitem exatamente esta espécie do paralelismo dentro do programa.
Possivelmente o melhor exemplo de enfiar (ou falta dele) é um Navegador da Web. O seu browser pode carregar de um número indefinido de arquivos e Páginas da Web ao mesmo tempo lhe permitindo ainda continuar pesquisando? Enquanto estas páginas carregam, o seu browser pode carregar de todos os quadros, sons, e assim por diante na paralela, intercalando os tempos de carregamento rápidos e lentos de múltiplos Servidores de internet? Os browseres multiroscados podem fazer todas estas coisas em virtude do seu uso interno de fios.
Hoje aprenderá sobre as seguintes questões primárias que rodeiam fios:
Vamos começar a lição de hoje definindo qual um fio é.
O suporte de multifilamento em Java gira em volta do conceito de um fio. Assim, o que exatamente é um fio? Posto simplesmente, um fio é uma corrente única da execução dentro de um processo. Okey, talvez não foi tão simples. Poderia ser melhor começar explicando exatamente qual um processo é. Um processo é uma execução de programa dentro do seu próprio espaço de endereços. Java é um sistema de multiprocessamento, significando que apoia muitos processos que correm concorrentemente nos seus próprios espaços de endereços. Pode ser mais familiar com o termo multiparalelização, que descreve um cenário muito semelhante ao multiprocessamento. Como um exemplo, considere a variedade de aplicações que tipicamente correm ao mesmo tempo em um ambiente gráfico. A maior parte de usuários do Windows 95 tipicamente dirigem várias aplicações em conjunto ao mesmo tempo, como Microsoft Word, Leitor de cd, Transmissão de mensagens de Windows, Controle de volume, e naturalmente Solitário. Estas aplicações são toda a execução de processos dentro do ambiente do Windows 95. Portanto pode pensar em processos como análogos a aplicações ou programas autônomos; cada processo em um sistema dá-se o seu próprio espaço na memória para realizar.
Um fio é uma sequência da execução de código dentro do contexto de um processo. Em verdade, os fios não podem realizar sozinhos; necessitam que o de cima de um processo de pais corra. Dentro de cada um dos processos que tipicamente correm, não há dúvida há vária execução de fios. Por exemplo, o Word pode ter um fio em background automaticamente verificação da ortografia do que se está escrevendo, enquanto outro fio pode estar salvando automaticamente modificações do documento. Como Word, cada aplicação (processo) pode estar dirigindo muitos fios que executam qualquer número de tarefas. A significação aqui consiste em que os fios sempre se associam com um determinado processo.
Julgando pelo fato que descrevi fios e processos usando o Windows 95 como um exemplo, adivinhou provavelmente que Java não é o primeiro sistema a empregar o uso de fios. Isto é verdade, mas Java é a primeira linguagem de programação principal a incorporar fios no coração da própria língua. Tipicamente, os fios implementam-se ao nível de sistema, necessitando uma interface de programação específica para a plataforma separada da linguagem de programação principal. Desde que Java apresenta-se como tanto uma língua como um sistema em tempo de execução, os arquitetos de Sol foram capazes de integrar fios em ambos. O resultado de fim consiste em que é capaz de utilizar fios de Java em um padrão, transversal plataforma moda.
Se o filamento for tão maravilhoso, porque cada sistema não o tem? Muitos sistemas operacionais modernos mandam precisar dos primitivos básicos para criar e dirigir fios, mas faltam a um ingrediente-chave: O resto do seu ambiente não é fio seguro. Um ambiente seguro do fio é aquele que permite a fios coexistir seguramente um com outro pacificamente. Suponha que está em um fio, um de muitos, e cada um de vocês compartilha alguns dados importantes dirigidos pelo sistema. Se dirigia aqueles dados, pode tomar medidas para protegê-los (como verá depois hoje), mas o sistema os dirige. Agora visualize uma parte do código no sistema que lê algum valor crucial, pensa nele durante algum tempo, e logo acrescenta 1 ao valor:
if (crucialValue > 0) { . . . // think about what to do crucialValue += 1; }
Lembre-se de que qualquer número de fios pode estar convidando esta parte do sistema ao mesmo tempo. O desastre ocorre quando dois fios realizaram ambos o teste de if antes que qualquer tenha incrementado crucialValue. Neste caso, o valor bate-se por ambos eles com o mesmo crucialValue += 1, e um dos incrementos perdeu-se. Isto pode não parecer assim mau na superfície, mas imaginar se o valor crucial afeta o estado da tela como se está expondo. Agora, a ordenação inoportuna dos fios pode fazer que à tela se atualize incorretamente. De mesmo modo, o rato ou os eventos de teclado podem perder-se, os bancos de dados podem atualizar-se inexatamente, e a destruição geral pode seguir.
Este desastre é inescapável se alguma parte significativa do sistema não se tenha escrito com fios em mente. Nisso está a razão porque há ambientes enfiados de pouca corrente principal - o grande esforço necessitado reescrever bibliotecas existentes da segurança de fio. Afortunadamente, Java escreveu-se do zero com isto é mente, e cada classe de Java na sua biblioteca é fio seguro. Assim, agora tem de incomodar-se só com a sua própria sincronização e problemas encomendam o fio porque pode supor que o sistema de Java faça a coisa direita.
As seções sincronizadas do código chamam-se seções críticas, contendo que o acesso a eles é crítico da execução roscada bem sucedida do programa. As seções críticas são referidas também às vezes como operações atômicas, significando que aparecem a outros fios como se ocorram ao mesmo tempo. Em outras palavras, tão como um átomo é uma unidade discreta da matéria, as operações atômicas efetivamente atuam como uma operação discreta a outros fios, embora realmente possam conter muitas operações no interior.
As seções críticas ou operações atômicas, são seções sincronizadas do código que parecem acontecer "de repente" - exatamente no mesmo tempo - a outros fios. Isto resulta em só um fio que é capaz de acessar o código em uma seção crítica de uma vez.
Observar |
Alguns leitores podem admirar-se qual o problema fundamental realmente é. Somente não pode fazer a área de ... no exemplo prévio mais pequena e mais pequena para reduzir ou eliminar o problema? Sem operações atômicas, a resposta é não. Mesmo se o ... levou um tempo, deve olhar primeiro para o valor de alguma variável para tomar qualquer decisão e logo modificar algo para refletir aquela decisão. Estes dois passos nunca podem fazer-se para acontecer ao mesmo tempo sem uma operação atômica. A menos que você dê um o sistema, é literalmente impossível criar o seu próprio. Mesmo uma linha crucialValue += 1 implica três passos: adquira o valor atual, acrescente-lhe aquele e guarde-o atrás. (A utilização de ++crucialValue não ajuda também.) Os três passos têm de resultar "de repente" (atômicamente) estar seguros. Os primitivos de Java especiais, aos níveis mais baixos da língua, provêem-no das operações atômicas básicas tem de construir programas seguros, roscados. |
Acostumar-se a fios toma curto espaço de tempo e uma nova maneira de pensar. Em vez de supor que sempre sabe exatamente o que acontece quando olha para um método escreveu, tem de fazer-se algumas perguntas adicionais. O que acontecerá se mais de um fio chamar neste método ao mesmo tempo? Precisa de protegê-lo de algum modo? Que tal a sua classe no conjunto? Está supondo que só um dos seus métodos esteja correndo ao mesmo tempo?
Muitas vezes faz tais suposições, e uma variável de exemplo local vai se estragar como isso. Desde que o saber comum dita que aprendemos dos nossos erros, vamos fazer alguns erros e logo tente corrigi-los. Em primeiro lugar, aqui está o caso mais simples:
public class ThreadCounter { int crucialValue; public void countMe() { crucialValue += 1; } public int howMany() { return crucialValue; } }
Este código mostra uma classe usada para contar fios que sofre da forma mais pura do "problema de sincronização": O += dá mais de um passo, e pode miscount o número de fios como isso. (Não se incomode com como os fios ainda se criam; somente suponha que um ramo inteiro deles é capaz de chamar countMe(), ao mesmo tempo, mas em tempos ligeiramente diferentes.) Java permite-lhe fixar esta situação:
public class SafeThreadCounter { int crucialValue; public synchronized void countMe() { crucialValue += 1; } public int howMany() { return crucialValue; } }
A palavra-chave de synchronized diz a Java fazer o bloco do código no fio de método seguro. Isto significa que só um fio se permitirá dentro deste método ao mesmo tempo, e os outros terão de esperar até que o fio atualmente corrente se termine com ele antes que possam começar a dirigi-lo. Isto contém que sincronizar um método grande, que corre muito tempo quase sempre é uma má ideia. Todos os seus fios terminariam segurou neste gargalo, esperando o arquivo único para adquirir a sua volta neste método lento.
É até pior do que poderia pensar para variáveis não sincronizadas. Como o compilador pode guardá-los em volta em registros de CPU durante os cômputos, e os registros de um fio não podem ver-se por outros fios, uma variável pode atualizar-se de tal modo que nenhuma ordem possível de atualizações de fio pode ter produzido o resultado. Isto é completamente incompreensível ao programador, mas pode acontecer. Para evitar este caso grotesco, pode etiquetar um volatile variável, subentendendo que sabe que se atualizará assincronamente por fios parecidos a um multiprocessador. Java então carrega-o e guarda-o cada vez quando é necessário e não usa registros de CPU.
Observar |
Supõe-se que todas as variáveis sejam fio seguro a menos que especificamente os marque como volatile. Tenha em mente que a utilização de volatile é um evento extremamente raro. De fato, no 1.0.2 lançamento, Java API não usa volatile em nenhuma parte. |
O howMany() de método no exemplo último não precisa de sincronizar-se porque simplesmente devolve o valor atual de uma variável de exemplo. Um método mais alto na chamada encadeia aquela que usa o valor devolvido de howMany() - precisaria de sincronizar-se, entretanto. A listagem 18.1 contém um exemplo de um fio na necessidade deste tipo da sincronização.
A listagem 18.1. A classe de Point.
1: public class Point { //redefines class Point from package java.awt 2: private float x, y; //OK since we're in a different package here 3: 4: public float x() { // needs no synchronization 5: return x; 6: } 7: 8: public float y() { // ditto 9: return y; 10: } 11: . . . // methods to set and change x and y 12: } 13: 14: public class UnsafePointPrinter { 15: public void print(Point p) { 16: System.out.println("The point's x is " + p.x() 17: + " and y is " + p.y() + "."); 18: } 19: }
Os métodos análogos a howMany() são x() e y(). Não precisam de nenhuma sincronização porque somente devolvem os valores de variáveis de membro. É a responsabilidade do chamador de x() e y() para decidir se tem de sincronizar-se - e neste caso, faz. Embora o método print() simplesmente leia valores e os imprima, lê dois valores. Isto significa que há uma possibilidade que algum outro fio, que corre entre a chamada a p.x() e a chamada a p.y(), pode ter modificado o valor de x e y guardado dentro do Point p. Lembre-se, não sabe quantos outros fios têm um modo de conseguir e chamar métodos neste objeto de Point! "O pensamento multienfiou" alcança ser cuidadoso qualquer tempo faz uma suposição que algo não aconteceu entre duas partes do seu programa (até duas partes da mesma linha ou a mesma expressão, como a cadeia expressão de + neste exemplo).
Pode tentar fazer uma versão segura de print() acrescentando-lhe simplesmente o modificador de palavra-chave de synchronized, mas em vez disso, vamos tentar uma abordagem ligeiramente diferente:
public class TryAgainPointPrinter { public void print(Point p) { float safeX, safeY; synchronized(this) { safeX = p.x(); // these two lines now safeY = p.y(); // happen atomically } System.out.print("The point's x is " + safeX + " y is " + safeY); } }
A afirmação de synchronized toma um argumento que diz o que objeta que você gostasse de trancar para impedir mais de um fio de realizar o bloco cercado do código ao mesmo tempo. Aqui, usa this (o próprio exemplo), que é exatamente o objeto que se teria trancado pelo método de synchronized no conjunto se tivesse modificado print() para parecer-se com o seu método de countMe() seguro. Tem um bônus acrescentado com esta nova forma da sincronização: pode especificar exatamente que parte de um método tem de estar segura, e o resto pode deixar-se perigoso.
Note como se aproveitou desta liberdade de fazer a parte protegida do método o menor possível, deixando as criações de String, concatenações, e imprimindo (que em conjunto tomam um período de tempo pequeno mas finito) do lado de fora da área "protegida". Isto é ambos o bom estilo (como um guia do leitor do seu código) e mais eficiente, porque menos fios emperram esperando para entrar em áreas protegidas.
O leitor astuto, entretanto, ainda pode preocupar-se pelo exemplo último. Parece como se se assegurasse que ninguém realiza as suas chamadas a x() e y() fora da ordem, mas impediu o Point p de modificar-se fora de abaixo de você? Se a resposta não for, ainda não resolvia completamente o problema. Resulta que realmente precisa do poder total da afirmação de synchronized:
public class SafePointPrinter { public void print(Point p) { float safeX, safeY; synchronized(p) { // no one can change p safeX = p.x(); // while these two lines safeY = p.y(); // are happening atomically } System.out.print("The point's x is " + safeX + " y is " + safeY); } }
Agora tem-no! De fato tinha de proteger o Point p de modificações, portanto o tranca por contanto que ele como o argumento à sua afirmação de synchronized. Agora quando x() e y() se chamam em conjunto, com certeza adquirirão o x atual e y do Point p, sem qualquer outro fio que é capaz de chamar um método de modificação entre. Ainda está supondo, contudo, que o Point p se tenha protegido propriamente. Sempre pode assumir isto sobre classes de sistema - mas escreveu esta classe de Point. Pode assegurar-se que é okey escrevendo o único método que pode modificar x e y dentro de p você mesmo:
public class Point { private float x, y; . . . // the x() and y() methods public synchronized void setXAndY(float newX, float newY) { x = newX; y = newY; } }
Fazendo synchronized o único método "de jogo" em Point, garante que qualquer outro fio que tenta agarrar o Point p e modificá-lo fora de abaixo de você tem de esperar. Trancou o Point p com a sua afirmação de synchronized(p), e qualquer outro fio tem de trancar o mesmo Point
p via a afirmação de synchronized(this) implícita que se realiza quando p entra em setXAndY(). Então finalmente é fio seguro.
Observar |
A propósito, se Java tinha algum modo de devolver mais de um valor ao mesmo tempo, pode escrever um método de synchronized getXAndY() de Point que devolve ambos os valores seguramente. Na língua de Java atual, tal método pode devolver um Point novo, único para garantir aos seus chamadores que ninguém mais tem uma cópia que poderia modificar-se. Este tipo do truque pode usar-se para minimizar as partes do sistema que tem de incomodar-se com a sincronização. |
Suponha que quer que uma variável de classe reúna alguma informação através de todo os exemplos de uma classe:
public class StaticCounter { private static int crucialValue; public synchronized void countMe() { crucialValue += 1; } }
Isto está seguro? Se crucialValue fosse uma variável de exemplo, seria. Como é uma variável de classe, contudo, e há só uma cópia dele para todos os exemplos; ainda pode ter múltiplos fios que o modificam usando exemplos diferentes da classe. (Lembre-se de que o modificador de synchronized tranca o objeto de this - um exemplo.) Afortunadamente, agora sabe a técnica necessitada resolver isto:
public class StaticCounter { private static int crucialValue; public void countMe() { synchronized(getClass()) { // can't directly name StaticCounter crucialValue += 1; // the (shared) class is now locked } } }
O truque deve "trancar" em um objeto diferente - não em um exemplo da classe, mas na própria classe. Como uma variável de classe é "dentro" de uma classe, como uma variável de exemplo é dentro de um exemplo, isto não deve ser isto tudo inesperado. De um modo semelhante, as classes podem fornecer recursos globais que qualquer exemplo (ou outra classe) pode acessar diretamente usando o nome de classe e fechadura usando que o mesmo nome de classe. No exemplo último, crucialValue usou-se de dentro de um exemplo de StaticCounter, mas se crucialValue se declarasse por public em vez disso, de qualquer lugar no programa, estaria seguro dizer o seguinte:
synchronized(Class.forName("StaticCounter")) { StaticCounter.crucialValue += 1; }
Observar |
O uso direto da variável de membro (de objeto) de outra classe está o estilo-it's realmente mau usado aqui simplesmente para demonstrar um ponto rapidamente. StaticCounter forneceria normalmente um countMe() - como o método de classe do seu próprio para fazer este tipo do trabalho sujo. |
Pode começar agora a apreciar quanto trabalho a equipe de Java fez para você pensando todos estes pensamentos difíceis para todo ou cada classe (e método!) na biblioteca de classe de Java.
Agora que entende o poder (e os perigos) de ter muitos fios que correm ao mesmo tempo, como aqueles fios se criam de fato?
Aviso |
O próprio sistema sempre tem alguma gerência de fios de demônio, um dos quais faz constantemente a tarefa tediosa da coleção de lixo para você em background. Também há um fio de usuário principal que escuta para eventos do seu rato e teclado. Se não tiver cuidado, pode fechar às vezes este fio principal. Se fizer, nenhum evento se envia ao seu programa e parece ser morto. Uma boa regra do polegar consiste em que sempre que esteja fazendo algo que pode fazer-se em um fio separado, provavelmente deve ser. Os fios em Java são relativamente baratos para criar, dirigir, e destruir, então não os use demasiado frugalmente. |
Como há uma classe java.lang.Thread, poderia adivinhar que pode criar um fio do seu próprio subclassificando-o - e tem razão:
public class MyFirstThread extends Thread { // a.k.a., java.lang.Thread public void run() { . . . // do something useful } }
Agora tem um novo tipo do fio chamado MyFirstThread, que faz algo útil quando o seu método de run() se chama. Naturalmente, ninguém criou este fio ou chamou o seu método de run(), portanto neste ponto é somente uma classe ansiosa por tornar-se um fio. Para criar de fato e dirigir um exemplo da sua nova classe de fio, escreve o seguinte:
MyFirstThread aMFT = new MyFirstThread(); aMFT.start(); // calls our run() method
O que pode ser mais simples? Cria um novo exemplo da sua classe de fio e logo pede que ele comece a correr. Sempre que queira parar o fio, faz isto:
aMFT.stop();
Além da resposta a start() e stop(), um fio também pode suspender-se temporariamente e depois retomar-se:
Thread t = new Thread(); t.suspend(); . . . // do something special while t isn't running t.resume();
Um fio vai automaticamente suspend() e logo resume() quando se bloqueia primeiro em um ponto sincronizado e logo depois se desbloqueia (quando é que "a tendência" de fio para correr).
Isto está tudo bem e bom se cada vez quiser criar um fio tem o luxo de ser capaz de colocá-lo abaixo da classe de Thread na herança única árvore de classe de Java. Mas e se mais naturalmente pertence abaixo de alguma outra classe, de que tem de herdar a maioria da sua implementação? As interfaces sobre as quais aprendeu no Dia 16, "Pacotes e Interfaces", vêm ao resgate:
public class MySecondThread extends ImportantClass implements Runnable { public void run() { . . . // do something useful } }
Implementando a interface Runnable, declara a sua intenção de correr em um fio separado. De fato, a classe de Thread é uma implementação desta interface, como poderia esperar das discussões de desenho sobre o Dia 16. Como também poderia adivinhar do exemplo, a interface de Runnable define só um método: run(). Como em MyFirstThread, espera que alguém crie um exemplo de um fio e de qualquer maneira chamar o seu método de run(). Aqui está como isto se realiza usando a aproximação de interface de enfiar a criação:
MySecondThread aMST = new MySecondThread(); Thread aThread = new Thread(aMST); aThread.start(); // calls our run() method, indirectly
Em primeiro lugar, cria um exemplo de MySecondThread. Então, passando este exemplo ao construtor que cria o novo fio, fá-lo o objetivo daquele fio. Sempre que aquele novo fio lance, o seu método de run() chama o método de run() do objetivo que se deu (suposto pelo fio ser um objeto que implementa a interface de Runnable). Quando start() se chama em aThread, o seu método de run() chama-se indiretamente. Pode parar aThread com stop(). Se não precisar de usar o objeto de Thread ou o exemplo de MySecondThread explicitamente, aqui está um atalho de uma linha:
new Thread(new MySecondThread()).start();
Observar |
Como pode ver, o nome de classe que MySecondThread é um bocado de um erro de nome - não desce de Thread, nem é de fato o fio que você start() e stop(). Pode ter-se chamado MySecondThreadedClass ou ImportantRunnableClass para ser mais claro neste ponto. |
A listagem 18.2 contém um exemplo mais longo de criação e utilização de fios.
A listagem 18.2. A classe de SimpleRunnable.
1: public class SimpleRunnable implements Runnable { 2: public void run() { 3: System.out.println("in thread named '" 4: + Thread.currentThread().getName() + "'"); 5: } // any other methods run() calls are in current thread as well 6: } 7: 8: public class ThreadTester { 9: public static void main(String argv[]) { 10: SimpleRunnable aSR = new SimpleRunnable(); 11: 12: while (true) { 13: Thread t = new Thread(aSR); 14: 15: System.out.println("new Thread() " + (t == null ? 16: "fail" : "succeed") + "ed."); 17: t.start(); 18: try { t.join(); } catch (InterruptedException ignored) { } 19: // waits for thread to finish its run() method 20: } 21: } 22: }
Observar |
Pode preocupar-se que só um exemplo da classe SimpleRunnable cria-se, mas muitos novos fios o usam. Não se confundem? Lembre-se de separar na sua mente o exemplo de aSR (e os métodos que entende) de vários fios da execução que pode passar por ele. os métodos de aSR fornecem um padrão para a execução, e múltiplos fios criados compartilham aquele padrão. Cada um lembra-se onde realiza e tudo o que mais tem de fazê-lo distinto de outros fios de gerência. Todos eles compartilham o mesmo exemplo e os mesmos métodos. Por isso tem de ter tanto cuidado, acrescentando a sincronização, para imaginar fios numerosos que correm exuberante sobre cada um dos seus métodos. |
O currentThread() de método de classe pode chamar-se para adquirir o fio no qual um método realiza atualmente. Se a classe de SimpleRunnable fosse uma subclasse de Thread, os seus métodos saberiam a resposta já (é a gerência de fio). Como SimpleRunnable simplesmente implementa a interface Runnable, contudo, e conta com alguém mais (ThreadTester main()) para criar o fio, o seu método de run() precisa de outro modo de adquirir as suas mãos sobre aquele fio. Muitas vezes, serão métodos profundamente interiores chamados pelo seu método de run() quando repentinamente tiver de adquirir o fluxo corrente. O método de classe mostrado nos trabalhos de exemplo, não importa onde é.
O exemplo então chama getName() no fluxo corrente para adquirir o nome do fio (normalmente algo útil, como Thread-23) portanto pode dizer o mundo em que fio run() corre. A coisa final a observar é o uso do método join(), que, quando enviado a um fio, significa que "planejo esperar para sempre por você para terminar o seu método de run()". Não quer usar esta aproximação sem boa razão: Se tiver algo mais importante tem de fazer-se no seu fio em breve, não pode contar com quanto tempo o fio juntado poderia tomar para terminar. No exemplo, o método de run() é curto e termina rapidamente, portanto cada laço pode esperar seguramente pelo fio prévio para morrer antes de criar o seguinte. Aqui está a produção produzida:
new Thread() succeeded. in thread named 'Thread-1' new Thread() succeeded. in thread named 'Thread-2' new Thread() succeeded. in thread named 'Thread-3' ^C
Incidentemente, Ctrl+C apertou-se para interromper o programa, porque de outra maneira continuaria para sempre.
Aviso |
Pode fazer algumas coisas razoavelmente desastrosas com o seu conhecimento de fios. Por exemplo, se corre no fio principal do sistema e, porque pensa que está em um fio diferente, acidentalmente diz o seguinte: Thread.currentThread () .stop (); tem consequências inoportunas para o seu (logo para ser morto) programa! |
Se quiser que os seus fios tenham determinados nomes, pode destiná-los você mesmo usando outra forma do construtor de Thread:
public class NamedThreadTester { public static void main(String argv[]) { SimpleRunnable aSR = new SimpleRunnable(); for (int i = 1; true; ++i) { Thread t = new Thread(aSR, "" + (100 - i) + " threads on the wall..."); System.out.println("new Thread() " + (t == null ? "fail" : "succeed") + "ed."); t.start(); try { t.join(); } catch (InterruptedException ignored) { } } } }
Esta versão do construtor de Thread toma um objeto de objetivo, como antes, e uma cadeia, que denomina o novo fio. Aqui está a produção:
new Thread() succeeded. in thread named '99 threads on the wall...' new Thread() succeeded. in thread named '98 threads on the wall...' new Thread() succeeded. in thread named '97 threads on the wall...' ^C
A nomeação de um fio é um modo fácil de passá-lo alguma informação. Isto fluxos de informação do fio de pais à sua nova criança. Também é útil, para depurar objetivos, dar a fios nomes significativos (como network input) para que quando aparecem durante um erro - em um traço de pilha, para o exemplo - possa identificar facilmente que fio causou o problema. Também poderia pensar usar nomes para ajudar a agrupar ou organizar os seus fios, mas Java de fato o provê de uma classe de ThreadGroup para executar esta função.
A classe de ThreadGroup usa-se para dirigir um grupo de fios como uma unidade única. Isto provê-o de um meio de controlar finamente a execução de fio de uma série de fios. Por exemplo, a classe de ThreadGroup fornece stop, suspend e métodos de resume para controlar a execução de todos os fios no grupo. Os grupos de fio também podem conter outros grupos de fio, levando em conta uma hierarquia aninhada de fios. Outro benefício à utilização de grupos de fio é que podem impedir fios de ser capazes de afetar outros fios, que é útil para a segurança.
Vamos imaginar uma versão diferente do exemplo último, aquele que cria um fio e logo transmite o fio a outras partes do programa. Suponha que o programa então gostaria de saber quando aquele fio morre para que possa executar alguma operação de limpeza. Se SimpleRunnable foi uma subclasse de Thread, poderia tentar pegar stop() sempre que tenha enviado - mas olhada na declaração de Thread do método de stop():
public final void stop() { . . . }
O final aqui significa que não pode ignorar este método em uma subclasse. Em todo o caso, SimpleRunnable não é uma subclasse de Thread, portanto como este exemplo imaginado pode pegar possivelmente a morte do seu fio? A resposta deve usar a seguinte magia:
public class SingleThreadTester { public static void main(String argv[]) { Thread t = new Thread(new SimpleRunnable()); try { t.start(); someMethodThatMightStopTheThread(t); } catch (ThreadDeath aTD) { . . . // do some required cleanup throw aTD; // re-throw the error } } }
Entende a maioria desta magia da lição de ontem. Tudo que tem de saber é que se o fio criado no exemplo morrer, lança um erro da classe ThreadDeath. O código pega aquele erro e executa a limpeza necessária. Então relança o erro, permitindo ao fio morrer. O código de limpeza não se chama se as saídas de fio normalmente (o seu método de run() conclui), mas isto for perfeito; colocou isto a limpeza foi só necessária quando stop() se usou no fio.
Observar |
Os fios podem morrer de outros modos por exemplo, lançando exceções que ninguém pega. Nestes casos, stop() nunca se chama e o código prévio não é suficiente. Como as exceções inesperadas não podem sair para matar em nenhum lugar um fio, os programas multiroscados que cuidadosamente pegam e tratam todas as suas exceções são mais predizíveis e robustos, e são mais fáceis depurar. |
Poderia estar admirando-se como qualquer sistema de software pode enfiar-se realmente correndo em uma máquina com um CPU único. Se houver só um CPU físico em um sistema de computador, é impossível para mais de uma instrução de código de máquina realizar-se de uma vez. Isto significa que não importa como muito tenta racionar o comportamento de um sistema multiroscado, só um fio realmente se está realizando por cima de um tempo particular. A realidade é que multienfiando em um sistema de CPU único, como os sistemas a maioria de nós usa, está em melhor uma boa ilusão. As boas notícias são que a ilusão trabalha tão bem a maior parte do tempo que nos sentimos bastante cômodos no fato que múltiplos fios realmente correm na paralela.
A ilusão da execução de fio paralela em um sistema com um CPU único muitas vezes dirige-se dando a cada fio uma oportunidade de realizar um bocado do código regularmente. Esta aproximação conhece-se como timeslicing, que se refere ao modo que cada fio consegue que um pouco do tempo do CPU realize o código. Quando acelera este cenário inteiro a milhões de instruções por segundo, o efeito inteiro da execução paralela encontra bastante bem.
A tarefa geral de direção e execução múltiplos fios em um ambiente como isto conhece-se como planejamento. De mesmo modo, a parte do sistema que decide a ordenação em tempo real de fios chama-se o escalonador.
Normalmente, qualquer escalonador tem dois modos fundamentalmente diferentes de olhar para o seu emprego: planejamento de nonpreemptive e tempo de preempção cortando.
Com o planejamento de nonpreemptive, o escalonador dirige o fluxo corrente para sempre, precisando que o fio para dizê-lo explicitamente quando está seguro começar um fio diferente. Com o tempo de preempção cortando, o escalonador dirige o fluxo corrente até que tenha esgotado certa fração muito pequena de um segundo, e logo "o adquira", o suspenda e retome outro fio da seguinte fração muito pequena de um segundo.
O planejamento de Nonpreemptive é muito elegante, sempre pedindo permissão de planejar, e é bastante valioso em aplicações em tempo real extremamente limitadas no tempo onde interromper-se no momento incorreto, ou por muito tempo, pode significar quebrar um avião.
Contudo, os escalonadores mais modernos usam o tempo de preempção cortando porque geralmente fazia programas multienfiados de escrita muito mais fáceis. Em primeiro lugar, não força cada fio a decidir exatamente quando deve "produzir" o controle outro fio. Em vez disso, cada fio somente pode correr cegamente em, sabendo que o escalonador será justo sobre a oferta de todos os outros fios a sua possibilidade de correr.
Contudo, resulta que esta aproximação ainda é não o modo ideal de planejar fios; abandonou um pouco demasiado controle ao escalonador. O toque final que muitos escalonadores modernos acrescentam deve permitir-lhe destinar cada fio uma prioridade. Isto cria uma ordenação de total de todos os fios, fazendo alguns fios mais "importantes" do que outros. Ser prioridade mais alta muitas vezes significa que um fio mais muitas vezes se dirige ou para um período de tempo mais longo, mas sempre significa que pode interromper outro, fios de prioridade mais baixa, até antes que o seu "tempo de processamento" tenha vencido.
Um bom exemplo de um fio de prioridade baixa é o fio de coleção de lixo no sistema de tempo de execução de Java. Embora a coleção de lixo seja uma função muito importante, não é algo que quer hogging o CPU. Desde que o fio de coleção de lixo é um fio de prioridade baixa, bufa ao longo em background, libertando a memória como o processador o permite. Isto pode resultar na memória que se liberta um pouco mais devagar, mas permite mais fios limitados no tempo, como o fio de manejo de entrada de usuário, acesso cheio ao CPU. Pode estar admirando-se o que acontece se o CPU ficar ocupado e o coletor de lixo nunca vem para limpar a memória. O sistema em tempo de execução fica sem memória e choque? Não. Isto sobe um dos aspectos arrumados de fios e como trabalham. Se um fio de alta prioridade não puder acessar um recurso precisa, como memória, entra em um estado esperar até que a memória fique disponível. Quando toda a memória se vai, toda a gerência de fios entrará consequentemente em um estado esperar, por meio disso libertando o CPU para realizar o fio de coleção de lixo, que à sua vez liberta a memória. E o círculo da vida roscada continua!
O lançamento (1.0.2) de Java atual não especifica precisamente o comportamento do seu escalonador. Os fios podem ser prioridades destinadas, e quando uma escolha se faz entre vários fios que todos querem dirigir, as vitórias de fio da prioridade mais alta. Contudo, entre fios que são mesmo assim prioridade, o comportamento não se define bem. De fato, as plataformas diferentes nas quais Java atualmente corre têm comportamentos um pouco diferentes que se comportam mais como um escalonador de preempção e alguns mais como um escalonador nonpreemptive.
Observar |
Esta especificação incompleta do escalonador é terrivelmente aborrecida e, presumivelmente, vai se corrigir em um lançamento posterior. Não saber os detalhes finos de como o planejamento ocorre é perfeitamente muito bom, mas não sabendo se os fios de prioridade igual devem produzir explicitamente ou ficar em frente da gerência para sempre não é muito bom. Por exemplo, todos os fios que criou por enquanto são fios de prioridade igual portanto não sabe o seu comportamento de planejamento básico! |
Para descobrir que tipo de escalonador tem no seu sistema, prove o seguinte código:
public class RunnablePotato implements Runnable { public void run() { while (true) System.out.println(Thread.currentThread().getName()); } } public class PotatoThreadTester { public static void main(String argv[]) { RunnablePotato aRP = new RunnablePotato(); new Thread(aRP, "one potato").start(); new Thread(aRP, "two potato").start(); } }
Se o seu sistema empregar um escalonador nonpreemptive, este código resulta na seguinte produção:
one potato one potato one potato . . .
Esta produção continuará para sempre ou até que interrompa o programa. Para um escalonador de preempção que usa o tempo cortando, este código repetirá a linha one potato algumas vezes, seguido do mesmo número de linhas de two potato, repetidas vezes:
one potato one potato ... one potato two potato two potato ... two potato . . .
Esta produção também continuará para sempre ou até que interrompa o programa. E se quiser estar seguro que os dois fios se trocarão, apesar do tipo do escalonador de sistema? Reescreve RunnablePotato como se segue:
public class RunnablePotato implements Runnable { public void run() { while (true) { System.out.println(Thread.currentThread().getName()); Thread.yield(); // let another thread run for a while } } }
Ponta |
Normalmente teria de usar Thread.currentThread().yield() para adquirir as suas mãos sobre o fluxo corrente, e logo chamar yield(). Como este modelo é tanto comum, contudo, a classe de Thread pode usar-se como um atalho. |
O método de yield() explicitamente dá qualquer outro fio que quer dirigir uma possibilidade de começar a correr. (Se não há fios que esperam para correr, o fio que fez o yield() simplesmente continua.) No nosso exemplo, há outro fio que isto somente morre para dirigir, portanto quando agora realiza a classe ThreadTester, deve a produção o seguinte:
one potato two potato one potato two potato one potato two potato . . .
Esta produção será o mesmo apesar do tipo do escalonador que tem.
Para ver se as prioridades de fio trabalham no seu sistema, tente este código:
public class PriorityThreadTester { public static void main(String argv[]) { RunnablePotato aRP = new RunnablePotato(); Thread t1 = new Thread(aRP, "one potato"); Thread t2 = new Thread(aRP, "two potato"); t2.setPriority(t1.getPriority() + 1); t1.start(); t2.start(); // at priority Thread.NORM_PRIORITY + 1 } }
Ponta |
Os valores que representam as prioridades mais baixas, normais, e mais altas que os fios podem destinar-se guardam-se em membros de classe constantes da classe de Thread: Thread.MIN_PRIORITY, Thread.NORM_PRIORITY e Thread.MAX_PRIORITY. O sistema destina novos fios, à revelia, a prioridade Thread.NORM_PRIORITY. As prioridades em Java definem-se atualmente em uma variedade de 1 a 10, com 5 que é normal, mas não deve depender destes valores; use as variáveis de classe ou truques como um mostrado neste exemplo. |
Se one potato for a primeira linha da produção, o seu sistema não adquire prioridades de fio de utilização. Porque? Suponha que o primeiro fio (t1) acaba de começar a correr. Mesmo antes que tenha uma possibilidade de imprimir algo, ao longo vem um fio de prioridade mais alta (t2) que quer correr também. Aquele fio de prioridade mais alta deve adquirir (interrompem) o primeiro e adquirem uma possibilidade de imprimir two potato antes que t1 termine de imprimir algo. De fato, se usa a classe de RunnablePotato que nunca yield() s, t2 fica no controle para sempre, imprimindo linhas de two potato, porque é uma prioridade mais alta do que t1 e nunca produz o controle. Se usar a última classe de RunnablePotato (com yield()), a produção alterna linhas de one potato e two potato como antes, mas começando com two potato.
A listagem 18.3 contém um exemplo bom, ilustrativo de como os fios complexos se comportam.
A listagem 18.3. A classe de ComplexThread.
1: public class ComplexThread extends Thread { 2: private int delay; 3: 4: ComplexThread(String name, float seconds) { 5: super(name); 6: delay = (int) seconds * 1000; // delays are in milliseconds 7: start(); // start up ourself! 8: } 9: 10: public void run() { 11: while (true) { 12: System.out.println(Thread.currentThread().getName()); 13: try { 14: Thread.sleep(delay); 15: } catch (InterruptedException e) { 16: return; 17: } 18: } 19: } 20: 21: public static void main(String argv[]) { 22: new ComplexThread("one potato", 1.1F); 23: new ComplexThread("two potato", 1.3F); 24: new ComplexThread("three potato", 0.5F); 25: new ComplexThread("four", 0.7F); 26: } 27: }
Este exemplo combina o fio e o seu provador em uma classe única. O seu construtor cuida de nomeação e começo de si mesmo porque é agora um fio. O método de main() cria novos exemplos da sua própria classe porque a classe é uma subclasse de Thread. O método de run() também é mais complicado porque agora usa, pela primeira vez, um método que pode lançar uma exceção inesperada.
O método de Thread.sleep() força o fluxo corrente para yield() e logo espera por pelo menos o período de tempo especificado para passar antes de permitir o fio correr novamente. Poderia interromper-se por outro fio, contudo, enquanto dorme. Em tal caso, lança um InterruptedException. Agora, porque run() não se define como lançando esta exceção, deve "esconder" o fato pegando-o e tratando-o você mesmo. Como as interrupções são normalmente pedidos de parar, deve sair o fio, que pode fazer voltando simplesmente do método de run().
Este programa deve a produção uma repetição mas o modelo complexo de quatro linhas diferentes, onde cada uma vez no longo espaço de tempo vê o seguinte:
. . . one potato two potato three potato four . . .
Deve estudar a produção de modelo para comprovar-se que o paralelismo verdadeiro vai a programas Java interiores. Também pode começar a apreciar que, se até este jogo simples de quatro fios puder produzir tal comportamento complexo, muitos outros fios devem ser capazes da produção perto do caos se não cuidadosamente controlado. Afortunadamente, Java fornece a sincronização e bibliotecas seguras do fio tem de controlar aquele caos.
Hoje aprendeu que o multifilamento é desejável e potente, mas introduz muitos novos métodos dos problemas e as variáveis agora têm de proteger-se de conflitos de fio - que pode levar ao caos se não cuidadosamente controlado. "Pensando multienfiou", pode descobrir os lugares nos seus programas que necessitam que afirmações de synchronized (ou modificadores) os façam enfiar seguro. Uma série de exemplos de Point demonstra vários níveis da segurança que pode realizar, e demonstrações de ThreadTesters como as subclasses de Thread ou classes que implementam a interface de Runnable, se criam e corrida para gerar programas multienfiados.
Também aprendeu hoje como usar yield(), start(), stop(), suspend() e resume() nos seus fios, e como pegar ThreadDeath sempre que aconteça. Aprendeu sobre de preempção e planejamento de nonpreemptive, tanto com como sem prioridades, e como testar o seu sistema de Java para ver qual deles o seu escalonador usa.
Arma-se agora de bastante informação para escrever o mais complexo de programas: multiroscados. Como se torna mais cômodo com fios, pode começar a usar a classe de ThreadGroup ou os métodos de lista de Thread para adquirir as suas mãos sobre todos os fios no sistema e manipulá-los. Não tenha medo de experimentar; não pode quebrar permanentemente nada, e só aprende tentando.
Se forem tão importantes para Java, porque não têm fios aparecidos em todas as partes do livro inteiro? | |
De fato, têm. Cada programa autônomo escrito por enquanto "criou" pelo menos um fio, aquele no qual corre. (Naturalmente o sistema criou aquele fio para ele automaticamente.) | |
Como exatamente estes fios se tornam criados e corrida? Que tal applets? | |
Quando um programa Java autônomo simples lança, o sistema cria um fio principal, e o seu método de run() chama o seu método de main() para começar o seu programa - não faz nada para adquirir aquele fio. De mesmo modo, quando um applet simples carrega em um browser permitido por Java, um fio já se criou pelo browser, e o seu método de run() chama o seu init() e métodos de start() para começar o seu programa. Em qualquer caso, um novo fio de alguma espécie criou-se em algum lugar pelo próprio ambiente de Java. | |
Sei que o lançamento de Java atual ainda é um pouco frisado sobre o comportamento do escalonador, mas qual é a palavra do Sol? | |
Aqui está a pá, como retransmitido por Arthur van Hoff no Sol: De caminho os fios de horários de Java" … dependem da plataforma. É normalmente de preempção, mas não sempre tempo cortado. As prioridades não sempre se observam, dependendo da implementação subjacente". Esta cláusula final dá-lhe uma insinuação que toda esta confusão é um problema de implementação, e que em algum futuro lançamento, o desenho e a implementação serão ambos claros sobre o planejamento de comportamento. | |
Os meus amigos paralelos dizem-me que devo importunar com algo chamado "o impasse". Devo? | |
Não para programas multiroscados simples. Contudo, em programas mais complicados, uma das preocupações mais grandes realmente torna-se um de evitar uma situação na qual o fio de trancou um objeto e espera por outro fio para terminar, enquanto aquele outro fio espera pelo primeiro fio para lançar aquele mesmo objeto antes que possa terminar. Isto é um impasse - ambos os fios vão se picar para sempre. As dependências mútuas como esta implicação de mais de dois fios podem ser bastante intricadas, enroladas, e difíceis de localizar, muito menos retificar. São um dos desafios principais na escrita que o complexo multienfiou programas. |