Baixe o app para aproveitar ainda mais
Prévia do material em texto
Programação Concorrente Threads em Java Prof. Jorge Viana Doria Junior, M.Sc. Mestre em Informática DCC/IM/UFRJ jjunior@unicarioca.edu.br 1 Sumário Executar tarefas simultaneamente utilizando Threads. Entender o escalonador de Threads. Problemas com concorrência e sincronismos em Threads. Estratégias para solução de problemas com Threads. Exclusão Mútua Locks Objetos imutáveis Exercícios práticos Threads A maioria dos programas são escritos de modo sequencial com um ponto de início (método main( )), uma sequência de execuções e um ponto de término. Em qualquer dado instante existe apenas uma instrução sendo executada. O que são Threads? É um simples fluxo sequencial de execução que percorre um programa. Multithreading: o programador especifica que os aplicativos contêm fluxos de execução (threads), cada thread designando uma parte de um programa que pode ser executado simultaneamente com outras threads. Threads Um thread não é somente um programa, mas executa dentro de um programa. É um fluxo único de controle sequencial dentro de um programa. Em várias situações, precisamos “rodar duas coisas ao mesmo tempo”. Imagine um programa que gera um relatório muito grande em PDF. É um processo demorado e, para dar alguma satisfação para o usuário, queremos mostrar uma barra de progresso. Queremos então gerar o PDF e ao mesmo tempo atualizar a barrinha. Pensando um pouco mais amplamente, quando usamos o computador também fazemos várias coisas simultaneamente: queremos navegar na internet e ao mesmo tempo ouvir música. A necessidade de se fazer várias coisas simultaneamente, ao mesmo tempo, paralelamente, aparece frequentemente na computação. Para vários programas distintos, normalmente o próprio sistema operacional gerencia isso através de vários processos em paralelo. Em um programa só (um processo só), se queremos executar coisas em paralelo, normalmente falamos de Threads. Threads Thread Processo Mais Leve Mais Pesado Recursos compartilhados Recursos Próprios(I/O, ...) Endereçamento compartilhado Endereçamento Próprio Ambiente de execução Compartilhada Ambiente de Execução próprio Existe dentro de um Processo Possui ao menos um thread O conceito de thread está intimamente ligado ao conceito de processo, assim é fundamental entender o que são processos, como eles são representados e colocados em execução pelo Sistema Operacional, para em seguida entender as threads. Benefícios de Thread A criação e terminação de uma thread nova é em geral mais rápida do que a criação e terminação de um processo novo. A comutação de contexto entre duas threads é mais rápido do que entre dois processos. A comunicação entre threads é mais rápida do que a comunicação entre processos. Multiprogramação usando o modelo de threads é mais simples e mais portável do que multiprogramação usando múltiplos processos. Implementação de uma Thread Existem duas formas de criar explicitamente um thread em Java: Estendendo a classe Thread e instanciando um objeto desta nova classe. Implementando a interface Runnable e passando um objeto desta nova classe como argumento do construtor da classe Thread. Nos dois casos a tarefa a ser executado pelo thread deverá ser escrita no método run( ). Implementação de uma Thread Em Java, usamos a classe Thread do pacote java.lang para criarmos linhas de execução paralelas. A classe Thread recebe como argumento um objeto com o código que desejamos rodar. Por exemplo, no programa de PDF e barra de progresso: Implementação de uma Thread public class GeraPDF { public void rodar( ) { // lógica para gerar o pdf... } } public class BarraDeProgresso { public void rodar( ) { // mostra barra de progresso e atualizando... } } Implementação de uma Thread E, no método main, criamos os objetos e passamos para a classe Thread. O método start é responsável por iniciar a execução da Thread: public class MeuPrograma { public static void main (String[] args) { GeraPDF gerapdf = new GeraPDF(); Thread threadDoPdf = new Thread(gerapdf); threadDoPdf.start(); BarraDeProgresso barraDeProgresso = new BarraDeProgresso(); Thread threadDaBarra = new Thread(barraDeProgresso); threadDaBarra.start(); } } Porém, o código não compilará. Como a classe Thread sabe que deve chamar o método public void rodar ( )? Como ela sabe que nome de método daremos e que ela deve chamar esse método especial? Falta na verdade um contrato entre as nossas classes a serem executadas e a classe Thread. Implementação de uma Thread Esse contrato existe e é feito pela interface Runnable: devemos dizer que nossa classe é “executável” e que segue esse contrato. Na interface Runnable, há apenas um método chamado run( ). Basta implementá-lo, “assinar” o contrato e a classe Thread já saberá executar nossa classe: Implementação de uma Thread public class GeraPDF implements Runnable { @Override public void run( ) { // lógica para gerar o pdf... } } public class BarraDeProgresso implements Runnable { @Override public void run( ) { // mostra barra de progresso e atualizando... } } Implementação de uma Thread A classe Thread recebe no construtor um objeto que é um Runnable, e seu método start chama o método run da nossa classe. Repare que a classe Thread não sabe qual é o tipo específico da nossa classe; para ela, basta saber que a classe segue o contrato estabelecido e possui o método run. É o bom uso de interfaces, contratos e polimorfismo na prática! Implementação de uma Thread A classe Thread implementa Runnable. Então, você pode criar uma subclasse dela e reescrever o run que, na classe Thread, não faz nadasse. public class GeraPDF extends Thread { public void run( ) { // to do: } } Implementação de uma Thread E, como nossa classe é uma Thread, podemos usar o start diretamente: GeraPDF gera = new GeraPDF(); gera.start( ); Apesar de ser um código mais simples, você está usando herança apenas por “preguiça” (herdamos um monte de métodos mas usamos apenas o run), e não por polimorfismo, que seria a grande vantagem. Prefira implementar Runnable a herdar de Thread. Solução Baseada em Herança da Classe Thread Thread GeraPDF instanciação Implementação de uma Thread public class Programa implements Runnable { private int id; public void setId(int id) { this.id = id; } @Override public void run( ) { for (int i = 0; i < 10000; i++) { System.out.println("Programa " + id + " valor: " + i); } } } Implementação de uma Thread public class Threads { public static void main(String[] args) { Thread t1 = new Thread(new Programa()); t1.start(); Thread t2 = new Thread(new Programa()); t2.start(); } } Solução Baseada na Interface Runnable Thread Programa Runnable referência Classe A Exercícios práticos 1) Crie a classe GeradorRelatorio, que contém o código a ser executado por uma thread: Exercícios práticos 2) Implemente o código para Criação e execução de uma Thread, usando um objeto GeradorRelatorio: No exercício 01, foi criada a classe GeradorRelatorio, que implementa Runnable; No exercício 02, criamos uma Thread 8 que, ao iniciar(thread.start()), invoca o método GeradorRelatorio.run(); Qual o resultado gerado pelo próximo exercício? Exercícios práticos 3) Altere o código de execução de thread, desenvolvido no exercício anterior, incluindo a mensagem final: Resultado da execução Por que a mensagem final aparece antes da execução da Thread? Thread == novo processo independente Resposta: Porque as Threads agem como processos independentes. Podemos solicitar a execução de vários processos ao mesmo tempo - Multithread; Vamos criar uma Thread para “exibir” uma mensagem, enquanto o relatório é processado; Exercícios práticos 4) Crie a classe BarraDeProgresso, que será executada por umaThread, durante a impressão do relatório: Exercícios práticos 5) Agora, nossa aplicação deve imprimir o relatório e exibir a barra, ao mesmo tempo: Resultado da execução Os processos concorrem por tempo de execução no processador; As regras de escalonamento definem o tempo de execução de cada processo. Vimos que as Threads agem como processos independentes; Podemos solicitar a execução de vários processos ao mesmo tempo - Multithread; Vamos criar uma Thread para “exibir” uma mensagem, enquanto o relatório é processado; Escalonador e trocas de contexto É serviço do escalonador de threads(scheduler) alternar o tempo de execução de cada Thread iniciada(start). A troca de contexto ocorre quando o escalonador salva o estado da thread atual, para recuperar depois, e pára sua execução. Neste momento, é iniciada uma nova thread. ou é recuperado o estado de outra que estava parada, voltando a executar; O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execução desta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essas escolhas (embora possamos dar “dicas” ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Você pode pensar que é ruim não saber a ordem. Mas perceba que se a ordem importa para você, se é importante que determinada coisa seja feita antes de outra, então não estamos falando de execuções paralelas, mas sim de um programa sequencial normal (onde uma coisa é feita depois da outra, em uma sequência). Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. E mais de um processador? A VM do Java e a maioria dos SOs modernos consegue fazer proveito de sistemas com vários processadores ou multi-core. A diferença é que agora temos mais de um processador executando coisas e teremos, sim, execuções verdadeiramente paralelas. Mas o número de processos no SO e o número de threads paralelas costumam ser tão grandes que, mesmo com vários processadores, temos as trocas de contexto. A diferença é que o escalonador tem dois ou mais processadores para executar suas threads. Mas dificilmente terá uma máquina com mais processadores que threads paralelas executando. Escalonador e trocas de contexto Se rodarmos o exemplo dos slides 17 e 18, qual será a saída? De um a mil e depois de um a mil? Provavelmente não, senão seria sequencial. Ele imprimirá 1 de t1, 1de t2, 2 de t1, 2 de t2, 3 de t1 e 3 de t2 e etc... Exatamente intercalado? Na verdade, não sabemos exatamente qual é a saída. Se executar o programa várias vezes, observará que em cada execução a saída é um pouco diferente. Ciclo de vida O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execução desta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essas escolhas (embora possamos dar “dicas” ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Você pode pensar que é ruim não saber a ordem. Mas perceba que se a ordem importa para você, se é importante que determinada coisa seja feita antes de outra, então não estamos falando de execuções paralelas, mas sim de um programa sequencial normal (onde uma coisa é feita depois da outra, em uma sequência). Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. E mais de um processador? A VM do Java e a maioria dos SOs modernos consegue fazer proveito de sistemas com vários processadores ou multi-core. A diferença é que agora temos mais de um processador executando coisas e teremos, sim, execuções verdadeiramente paralelas. Mas o número de processos no SO e o número de threads paralelas costumam ser tão grandes que, mesmo com vários processadores, temos as trocas de contexto. A diferença é que o escalonador tem dois ou mais processadores para executar suas threads. Mas dificilmente terá uma máquina com mais processadores que threads paralelas executando. Prioridades em Threads Cada thread possui uma prioridade de execução que vai de Thread.MIN_PRIORITY (igual a 1) a Thread.MAX_PRIORITY (igual a 10). Importante: uma thread herda a prioridade da thread que a criou. O algoritmo de escalonamento sempre deixa a thread (runnable) de maior prioridade executar. A thread de maior prioridade preempta as outras threads de menor prioridade. O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execuçãodesta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essas escolhas (embora possamos dar “dicas” ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Você pode pensar que é ruim não saber a ordem. Mas perceba que se a ordem importa para você, se é importante que determinada coisa seja feita antes de outra, então não estamos falando de execuções paralelas, mas sim de um programa sequencial normal (onde uma coisa é feita depois da outra, em uma sequência). Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. E mais de um processador? A VM do Java e a maioria dos SOs modernos consegue fazer proveito de sistemas com vários processadores ou multi-core. A diferença é que agora temos mais de um processador executando coisas e teremos, sim, execuções verdadeiramente paralelas. Mas o número de processos no SO e o número de threads paralelas costumam ser tão grandes que, mesmo com vários processadores, temos as trocas de contexto. A diferença é que o escalonador tem dois ou mais processadores para executar suas threads. Mas dificilmente terá uma máquina com mais processadores que threads paralelas executando. Escalonamento Fonte: Java, Como programar. Deitel, 6ª Edição O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execução desta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essas escolhas (embora possamos dar “dicas” ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Você pode pensar que é ruim não saber a ordem. Mas perceba que se a ordem importa para você, se é importante que determinada coisa seja feita antes de outra, então não estamos falando de execuções paralelas, mas sim de um programa sequencial normal (onde uma coisa é feita depois da outra, em uma sequência). Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. E mais de um processador? A VM do Java e a maioria dos SOs modernos consegue fazer proveito de sistemas com vários processadores ou multi-core. A diferença é que agora temos mais de um processador executando coisas e teremos, sim, execuções verdadeiramente paralelas. Mas o número de processos no SO e o número de threads paralelas costumam ser tão grandes que, mesmo com vários processadores, temos as trocas de contexto. A diferença é que o escalonador tem dois ou mais processadores para executar suas threads. Mas dificilmente terá uma máquina com mais processadores que threads paralelas executando. Prioridades em Threads Se todas as threads tiverem a mesma prioridade, a CPU é alocada para todos, um de cada vez, em modo round-robin. getPriority(): obtém a prioridade corrente da thread; setPriority(): define uma nova prioridade. O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execução desta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essas escolhas (embora possamos dar “dicas” ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Você pode pensar que é ruim não saber a ordem. Mas perceba que se a ordem importa para você, se é importante que determinada coisa seja feita antes de outra, então não estamos falando de execuções paralelas, mas sim de um programa sequencial normal (onde uma coisa é feita depois da outra, em uma sequência). Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. E mais de um processador? A VM do Java e a maioria dos SOs modernos consegue fazer proveito de sistemas com vários processadores ou multi-core. A diferença é que agora temos mais de um processador executando coisas e teremos, sim, execuções verdadeiramente paralelas. Mas o número de processos no SO e o número de threads paralelas costumam ser tão grandes que, mesmo com vários processadores, temos as trocas de contexto. A diferença é que o escalonador tem dois ou mais processadores para executar suas threads. Mas dificilmente terá uma máquina com mais processadores que threads paralelas executando. Prioridades em Threads class BaixaPrioridade extends Thread { public void run() { setPriority(Thread.MIN_PRIORITY); for(;;) { System.out.println("Thread de baixa prioridade executando -> 1"); } } } class AltaPrioridade extends Thread { public void run() { setPriority(Thread.MAX_PRIORITY); for(;;) { for(int i=0; i<5; i++) System.out.println("Thread de alta prioridade executando -> 10"); try { sleep(100);} catch(InterruptedException e) { System.exit(0); } } } } O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execução desta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essas escolhas (embora possamos dar “dicas” ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Você pode pensar que é ruim não saber a ordem. Mas perceba que se a ordem importa para você, se é importante que determinada coisa seja feita antes de outra, então não estamos falando de execuções paralelas, mas sim de um programa sequencial normal (onde uma coisa é feita depois da outra, em uma sequência). Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. E mais de um processador? A VM do Java e a maioria dos SOs modernos consegue fazer proveito de sistemas com vários processadores ou multi-core. A diferença é que agora temos mais de um processador executando coisas e teremos, sim, execuções verdadeiramente paralelas. Mas o número de processos no SO e o número de threads paralelas costumam ser tão grandes que, mesmo com vários processadores, temos as trocas de contexto. A diferença é que o escalonador tem dois ou mais processadores para executar suas threads. Mas dificilmente terá uma máquina com mais processadores que threads paralelas executando. Prioridades em Threads class Lançador { public static void main(String args[ ]) { AltaPrioridade a = new AltaPrioridade(); BaixaPrioridade b = new BaixaPrioridade(); System.out.println("Iniciando threads..."); b.start(); a.start(); // Deixa as outras threads iniciar a execução. // O método yield(), cede o processamento para outra thread. Thread.currentThread().yield(); System out println("Main feito"); } } Iniciando threads... Main feito Thread de alta prioridade executando -> 10 Thread de alta prioridade executando -> 10 Thread de baixa prioridade executando -> 1 Thread de baixa prioridade executando -> 1 Thread de baixa prioridade executando -> 1 Thread de alta prioridade executando -> 10 Thread de alta prioridade executando -> 10 Thread de baixa prioridade executando -> 1 O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execução desta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essas escolhas (embora possamos dar “dicas” ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Você pode pensar que é ruim não saber a ordem. Mas perceba que se a ordem importa para você, se é importante que determinada coisa seja feita antes de outra, então não estamos falando de execuções paralelas, mas sim de um programa sequencial normal (onde uma coisa é feita depois da outra, em uma sequência). Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. E mais de um processador? A VM do Java e a maioria dos SOs modernos consegue fazer proveito de sistemas com vários processadores ou multi-core. A diferença é que agora temos mais de um processador executando coisas e teremos, sim, execuções verdadeiramente paralelas. Mas o número de processos no SO e o número de threads paralelas costumam ser tão grandes que, mesmo com vários processadores, temos as trocas de contexto. A diferença é que o escalonador tem dois ou mais processadores para executar suas threads. Mas dificilmente terá uma máquina com mais processadores que threads paralelas executando. Problemas com Concorrência Sincronização de Threads Quando muitas threads são executadas muitas vezes é necessário sincronizar suas atividades. Por exemplo, prevenir o acesso concorrente a estruturas de dados (variáveis, vetores, matrizes etc.) no programa que são compartilhados entre as threads; O problema é que no computador existe apenas um processador capaz de executar coisas. E quando queremos executar várias coisas ao mesmo tempo, e o processador só consegue fazer uma coisa de cada vez? Entra em cena o escalonador de threads. O escalonador (scheduler), sabendo que apenas uma coisa pode ser executada de cada vez, pega todas as threads que precisam ser executadas e faz o processador ficar alternando a execução de cada uma delas. A ideia é executar um pouco de cada thread e fazer essa troca tão rapidamente que a impressão que fica é que as coisas estão sendo feitas ao mesmo tempo. O escalonador é responsável por escolher qual a próxima thread a ser executada e fazer a troca de contexto (context switch). Ele primeiro salva o estado da execução da thread atual para depois poder retomar a execução da mesma. Aí ele restaura o estado da thread que vai ser executada e faz o processador continuar a execução desta. Depois de um certo tempo, esta thread é tirada do processador, seu estado (o contexto) é salvo e outra thread é colocada em execução. A troca de contexto é justamente as operações de salvar o contexto da thread atual e restaurar o da thread que vai ser executada em seguida. Quando fazer a troca de contexto, por quanto tempo a thread vai rodar e qual vai ser a próxima thread a ser executada, são escolhas do escalonador. Nós não controlamos essasescolhas (embora possamos dar “dicas” ao escalonador). Por isso que nunca sabemos ao certo a ordem em que programas paralelos são executados. Você pode pensar que é ruim não saber a ordem. Mas perceba que se a ordem importa para você, se é importante que determinada coisa seja feita antes de outra, então não estamos falando de execuções paralelas, mas sim de um programa sequencial normal (onde uma coisa é feita depois da outra, em uma sequência). Todo esse processo é feito automaticamente pelo escalonador do Java (e, mais amplamente, pelo escalonador do sistema operacional). Para nós, programadores das threads, é como se as coisas estivessem sendo executadas ao mesmo tempo. E mais de um processador? A VM do Java e a maioria dos SOs modernos consegue fazer proveito de sistemas com vários processadores ou multi-core. A diferença é que agora temos mais de um processador executando coisas e teremos, sim, execuções verdadeiramente paralelas. Mas o número de processos no SO e o número de threads paralelas costumam ser tão grandes que, mesmo com vários processadores, temos as trocas de contexto. A diferença é que o escalonador tem dois ou mais processadores para executar suas threads. Mas dificilmente terá uma máquina com mais processadores que threads paralelas executando. Interferência entre Threads Erros introduzidos quando múltiplas threads acessam um dado compartilhado. Essa interferência pode fazer com que o resultado não seja esperado. 36 36 36 Interleaving Interferência acontece quando duas operações, rodando em threads diferentes, atuam sobre o mesmo objeto. Interleaving significa que os passos realizados pelas operações se sobrepõem De forma não-determinística. 37 37 Exercícios práticos Imagine que temos a necessidade de imprimir os nomes das frutas de um cesto; Queremos “dar um tempo” para a leitura dos usuário; Com isso, precisamos que, após a impressão de um nome, seja realizada uma pausa na execução; Para isso, usamos Thread.sleep(tempo). Exercícios práticos 6) Cria a cesta de frutas: public class CestaFrutas implements Runnable { @Override public void run() { //Criação da lista de frutas String [ ] ingredientes = {"Banana", "Mamão", "Maçã", "Abacate"}; System.out.println("Início do Run()"); //Impressão da lista de frutas for (String fruta : ingredientes) { System.out.println(fruta); //Dormindo por 3 segundos try { Thread.sleep(3*1000); }catch (InterruptedException e){ e.printStackTrace(); } } System.out.println("Fim do Run()"); } } Exercícios práticos 7) Implemente e execute o código a seguir: 8) Qual o resultado? public class ThreadCestaFrutas { public static void main(String[] args) { //Criação do objeto executável CestaFrutas salada = new CestaFrutas(); //Criação da Thread Thread executar = new Thread(salada); //Execução da Thread executar.start(); } } Exercícios práticos Consultando a documentação java, verificamos que a classe Thread implementa Runnable. Isso nos permite criar uma classe filha de Thread e sobrescrever o método run(). Isso nos permite colocar na mesma classe o executor e o executado. Exercícios práticos 8) Implemente a herança de Thread. É mais fácil criar uma classe filha de Thread do que que usar um objeto Runnable. Mas não é uma boa prática estender uma Thread. Com extends Thread, nossa classe ficaria muito limitada, não podendo herdar os componentes de outra. Garbage Collector O Garbage Collector (coletor de lixo) é uma Thread responsável por jogar fora todos os objetos que não estão sendo referenciados; Imaginemos o código a seguir: Garbage Collector Com a execução do item 1, são criados dois objetos ContaBancaria e atribuídos às referências conta1 e conta2. No item 2, as duas referências passam a apontar para o mesmo objeto. Neste momento, quantos objetos existem na memória? Um ou dois? Perdemos a referência a um dos objetos. Garbage Collector O objeto sem referência não pode mais ser acessado. Podemos afirmar que ele saiu da memória? Como Garbage Collector é uma Thread, por ser executado a qualquer momento. Com isso, dizemos que o objeto sem referência está disponível para coleta. Garbage Collector Você consegue executar o Garbage Collector. Mas chamando o método estático System.gc() você está sugerindo que a JVM rode o Garbage Collector naquele momento. Sua sugestão pode ser aceita ou não. Você não deve basear sua aplicação na execução do Garbage Collector. Sincronização entre threads em Java Problema: inconsistência de dados. Em outras palavras... Condição de corrida: operações sobre recursos compartilhados que podem levar a inconsistências dependendo da ordem de execução. Seção crítica: trecho do programa que contém operações que podem levar a inconsistências Solução: exclusão mútua Em outras palavras... Garantir atomicidade e acesso exclusivo ao recurso na seção crítica. Condições de Corrida Threads acessam uma variável compartilhada ao mesmo tempo. As threads escrevem e lêem a variável. O resultado de uma thread pode sobrescrever o da outra. O resultado da computação pode variar. 48 48 Race conditions é possível quando threads compartilham dados, lendo, escrevendo concorrentemente, e o resultado final pode variar, depende de quando a thread vai fazer o que. O resultado pode ser correto ou não. Para evitar isso temos que usar estratégias de sincronização. 48 Condições de corrida: Exemplo Thread T1: Pega o valor . Thread T2: Pega o valor . Thread T1: Incrementa o valor; resultado: 1. Thread T2: Decrementa o valor; resultado: -1 Thread T1: Armazena o resultado: 1. Thread T2: Armazena o resultado: -1. 49 49 Thread T1 chama o incrementar e Thread T2 chama o decrementar ao mesmo tempo, se o valor inicial do contador é zero, as ações intervaladas podem ser a sequência listada no slide. Por essa sequência, o resultado da Thread T1 é perdido. Essa sequência é uma das possíveis, podendo em outras circunstância, o resultado de T2 que pode ser Perdido. 49 Exemplo: código não sincronizado Entre a leitura e o armazenamento, a thread atual pode ser interrompida! Operação não-atômica! Valor de contador é lido (possivelmente guardado em um registrador) Valor lido é incrementado e armazenado (na memória) 50 50 Cada invocação do incrementar deve adicionar 1 ao contador e do decrementar deve subtrair 1.Porém se o objeto Contador for referenciado pro múltiplas threads, a interferência entre as threads pode fazer que esse comportamento não seja o esperado. 50 Seção Crítica (Região Crítica) Em programação concorrente, uma região crítica é uma área de código de um algoritmos que acessa um recurso compartilhado que não pode ser acessado concorrentemente por mais de uma linha de execução. Ocorre quando: Sistema composto por N processos (onde N > 1). Cada processo executa seu próprio código independente dos demais. Processos compartilham um dado recurso. O estado de um processo é desconhecido pelo outro. O estado/valor do recurso pode ser alterado. O objetivo é tornar a operação sobre o recurso compartilhado atômica, ou seja, Transação Atômica, em ciência da computação, é uma operação, ou conjunto de operações, em uma base de dados ou em uma estrutura de dados ou em qualquer outro sistema computacional, que deve ser executada completamente em caso de sucesso, ou ser abortada completamente em caso de erro. O exemplo clássico para a necessidade de uma "transação atômica" é aquele da transferência entre duas contas bancárias. No momento de uma transferência de valores de uma conta "A" para uma conta "B", que envolve pelo menos uma operações de ajuste no saldo para cada conta, se o computador responsável pela operação é desligado por faltade energia, espera-se que o saldo de ambas as contas não tenha se alterado. Neste caso são utilizados sistemas que suportam transações atômicas. Seção Crítica: Exemplo public void T0 ( ) { long i; for (i=0; i<1000000; i++) { a = a + 5; } System.out.println(“Encerrei a T0”); } public void T1 ( ) { long i; for (i=0; i<1000000; i++) { a = a + 2; } System.out.println(“Encerrei a T1”); } Seção Crítica: Exemplo Seção Crítica: Exemplo Seção Crítica: Exemplo Seção Crítica: Exemplo Seção Crítica: Exemplo Seção Crítica: Exemplo Uma estratégia simples de prevenção de interferência de threads e erros de consistência de memória. Campos final, que não podem ser modificados após a construção do objeto, podem ser lidos de maneira segura. Sincronização entre threads em Java 59 59 Métodos sincronizados permitem uma estratégia simples de prevenção de interferência de threads e erros de consistência na memória. Por exemplo, se um objeto é visível por mais de uma thread, toda leitura ou escrita nas variáveis deste objeto serão feitas através de métodos sincronizados. Uma importante exceção é que campos “final”, que não podem ser modificados após a construção do objeto, podem ser lidos de maneira segura, não precisando de métodos sincronizados. 59 Sincronização entre threads em Java Java oferece 2 formas básicas de sincronização: Métodos sincronizados Blocos sincronizados Métodos sincronizados Através da palavra synchronized 61 61 Reduzir o custo de sincronização associado ao método. Blocos sincronizados 62 62 62 Blocos sincronizados Poder combinar mais de um objeto no momento da sincronização. 63 63 Sincronização entre threads em Java Competição no acesso a dados compartilhados. Variável de classe (STATIC) e/ou Referência para um mesmo objeto Sincronização entre threads em Java Situação típica: duas ou mais threads fazem operação sobre dado compartilhado. Sincronização entre threads em Java Situação 1: T1 primeiro e T2 depois. Situação 1: Não há problemas de inconsistência... Sincronização entre threads em Java Situação 2: T1 e T2 concorrentemente. Situação 2: Há problemas de inconsistência... Exclusão Mútua em Java Compartilhamento do objeto. public static void main(String[] args) { ContaBancaria c = new ContaBancaria(1000f); ThreadDeposita td = new ThreadDeposita(c); ThreadRetira tr = new ThreadRetira(c); td.start(); tr.start(); } Exclusão Mútua em Java Thread faz acesso ao objeto. public class ThreadDeposita extends Thread { private ContaBancaria c; public ThreadDeposita(ContaBancaria c) { this.c = c; } public void run() { for (int i = 0; i < 5; i++) c.deposita(300f); } } Exclusão Mútua em Java Thread faz acesso ao objeto. public class ThreadRetira extends Thread { private ContaBancaria c; public ThreadRetira(ContaBancaria c) { this.c = c; } public void run() { for (int i = 0; i < 5; i++) c.retira(100f); } } Exclusão Mútua em Java Sem synchronized. class ContaBancaria { private float saldo; public ContaBancaria(float v) { saldo = v; } public float getSaldo() { return saldo; } void deposita(float v) { saldo += v; System.out.println("Depósito: R$ 300,00 - Saldo: " + getSaldo());} void retira(float v) { saldo -= v; System.out.println("Retirada: R$ 100,00 - Saldo: " + getSaldo());} } Exclusão Mútua em Java Uso de synchronized. class ContaBancaria { private float saldo; public ContaBancaria(float v) { saldo = v; } public float getSaldo() { return saldo; } synchronized void deposita(float v) { saldo += v; System.out.println("Depósito: R$ 300,00 - Saldo: " + getSaldo());} synchronized void retira(float v) { saldo -= v; System.out.println("Retirada: R$ 100,00 - Saldo: " + getSaldo());} } Métodos synchronized executam em exclusão mútua sobre o mesmo objeto compartilhado Exclusão Mútua em Java Métodos synchronized em um mesmo objeto são executados em exclusão mútua. Só têm efeito em objetos compartilhados (mais de 1 thread referenciando mesmo objeto). Limitação à concorrência. Usar com cuidado. Monitores São objetos que garantem a exclusão mútua na execução dos procedimentos associados a eles. Apenas um procedimento associado ao monitor pode ser executado em um determinado momento. Em Java todo objeto possui um monitor associado. 74 74 Monitores 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 procedimentos A e B estão associados a um monitor. Se no momento da invocação do procedimento A algum o procedimento B estiver sendo executando 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 procedimento A é "acordado" e sua execução retomada. O uso de monitores em Java é uma variação do proposto por Hoare. Na linguagem Java todo objeto possui um monitor associado. Para facilitar o entendimento podemos encarar o monitor como um detentor de um "passe". Toda thread pode pedir "emprestado" 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. Tentar fazer um desenho explicando isso: suponha que dois procedimentos A e B estão associados a um monitor. Se no momento da invocação do procedimento A algum o procedimento B estiver sendo executando 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 procedimento A é "acordado" e sua execução retomada. 74 Sincronização usando Monitores Declarando uma seção de código sincronizada 75 75 75 Sincronização usando Monitores Declarando o método todo sincronizado. 76 76 76 Sincronização usando Monitores Código com problemas no uso de monitores Sincronização usando Monitores Código corrigido: Sincronização usando Monitores Faz uso dos métodos Wait() e notify() para gerar eventos de espera e notificação para parar e esperar. Erros de consistência na memória Quando as threads possuem diferentes visões de um determinado dado. Exemplo: Uma variável inteiro compartilhada entre as threads int contador = 0; Thread A incrementa a variável contador++; Thread B imprime a variável System.out.println(contador); 80 80 Se os dois métodos fossem chamados na mesma thread em sequência, é seguro que o valor impresso seria 1, porém em threads diferente não é garantido que no momento da impressão o valor do contador já tenha sido incrementado pela outra thread; 80 Modificador volatile Variável pode ser acessada/modificada por mais de uma thread. O valor da variável nunca será guardado localmente pela thread. Garante que escrita e leitura são realizadas diretamente na memória principal. 81 81 81 Quando usar? Para escrever em uma variável, como uma flag, em uma thread. Para checar essa variável em outra thread. O valor de escrita não depende do valor corrente. Não há preocupações em perder o valor atualizado. 82 82 82 Para campos que são imutáveis (final). Para variáveis que são acessadas por uma única thread. Para operações complexas, como quando é necessário prevenir acesso a uma variável por um certo tempo. Quando NÃO usar? 83 83 83 Exemplo 84 84 Deadlock Descreve uma situação em que dois ou mais Threads são bloqueados para sempre esperando um ao outro. Ocorre quando várias threads necessitamdos mesmos bloqueios, mas os obtém em ordem diferente. Nas Multithreads em java podem ocorrer deadlocks porque a palavra-chave syncronized causa o bloqueio da thread em execução, enquanto aguarda o bloqueio associado ao objeto especificado. 85 85 Deadlock - Exemplo public class TestDeadlockExample1 { public static void main(String[] args) { final String resource1 = "ratan jaiswal"; final String resource2 = "vimal jaiswal"; // t1 tries to lock resource1 then resource2 Thread t1 = new Thread() { public void run() { synchronized (resource1) { System.out.println("Thread 1: Holding resource 1"); try { Thread.sleep(10); } catch (InterrupedException e) { } System.out.println("Thread 1: Waiting for resource 2"); synchronized (resource2) { System.out.println("Thread 1: Holding resources 1 & 2"); } } } }; 86 86 Deadlock - Exemplo // t2 tries to lock resource2 then resource1 Thread t2 = new Thread() { public void run() { synchronized (resource2) { System.out.println("Thread 2: Holding resource 2"); try { Thread.sleep(10); } catch (InterruptedException e) { } System.out.println("Thread 2: Waiting for resource 1"); synchronized (resource1) { System.out.println("Thread 2: Holding resource 1 & 2"); } } } }; t1.start(); t2.start(); } } Thread 1: Holding resource 1... Thread 1: Waiting for resource 2... Thread 1: Holding resource 1 & 2... Thread 2: Holding resource 1... Thread 2: Waiting for resource 2... Thread 2: Holding resource 1 & 2... 87 87 Deadlock - Solução // t2 tries to lock resource2 then resource1 Thread t2 = new Thread() { public void run() { synchronized (resource1) { System.out.println("Thread 2: Holding resource 1"); try { Thread.sleep(10); } catch (InterruptedException e) { } System.out.println("Thread 2: Waiting for resource 2"); synchronized (resource2) { System.out.println("Thread 2: Holding resource 1 & 2"); } } } }; t1.start(); t2.start(); } } Então, apenas modifique a ordem dos RESOURCES e das mensagens. Isso impede que o programa entre em deadlock. Thread 1: Holding resource 1... Thread 1: Waiting for resource 2... Thread 1: Holding resource 1 & 2... Thread 2: Holding resource 1... Thread 2: Waiting for resource 2... Thread 2: Holding resource 1 & 2... 88 88 Locks A interface Lock pode ser usada no lugar de blocos sincronizados . Oferecem maior flexibilidade. Para criar um lock explícito, você instancia uma implementação da interface Lock. Normalmente, instancia-se ReentrantLock Para obter o lock, usa-se o método lock() Para liberar o lock, usa-se unlock() 89 89 Locks can be used in place of synchronized blocks and to implement synchronized methods, that is, use Locks offer more flexibility than synchronized blocks in that a thread can unlock multiple locks it holds in a different order than the locks were obtained. This cannot be done with the implied locks of synchronized blocks because synchronized blocks must be lexically nested. Para criar um lock explícito, você instancia uma implementação da interface Lock Normalmente, instancia-se ReentrantLock Para obter o lock, usa-se o método lock() Para liberar o lock: usa-se unlock() Já que o lock não é liberado automaticamente no final de um método, pode ser útil usar try/finally para garantir que o lock seja liberado 89 public class Example { private Lock aLock = new ReentrantLock(); public int get(int who) { aLock.lock(); try{ //Acessar recursos compartilhados... } finally { aLock.unlock(); } } } Locks: Sintaxe Básica 90 90 Como garantir que o lock seja sempre liberado? Já que o lock não é liberado automaticamente, é essencial que o corpo do método esteja coberto por try/finally para garantir que o lock seja liberado Locks 91 91 91 Variáveis condicionais Para esperar por um lock explícito, cria-se uma variável condicional. Um objeto que implementa a interface Condition. Usar Lock.newCondition() para criar uma condição A condição provê os métodos: await() para esperar até a condição ser verdadeira signal() e signalAll() para avisar aos threads que a condição ocorreu. 92 92 Para esperar por um lock explícito, cria-se uma variável condicional Um objeto que implementa a interface Condition Usar Lock.newCondition() para criar uma condição Uma variável condicional não é um lock, mas sim uma variável associada a um lock. Variáveis condicionais são usadas no contexto de sincronização de dados. Variáveis condicionais geralmente tem uma API que possuem as mesma funcionalidades que os mecanismo de Java de esperar e notificar. Nestes mecanismos, as variáveis condicionais são na verdade o objeto lock que está protegendo. 92 Exemplo Blocos Guardados (Guarded Blocks) Thread frequentemente têm que coordenar suas ações. Guarded blocks é um mecanismo comum para coordenação. Um bloco só é executado caso uma determinada condição seja verdadeira. 94 94 Blocos Guardado com eficiência 95 Nota: sempre chame wait dentro de um loop que testa pela condição que está sendo esperada. Não assuma que a interrupção foi pela pela condição particular que você estava esperando, ou que a condição ainda seja verdadeira Note: Always invoke wait inside a loop that tests for the condition being waited for. Don't assume that the interrupt was for the particular condition you were waiting for, or that the condition is still true. 95 Notificando as threads Notificar é retirar do estado de espera. notify(): Notifica uma thread que esteja esperando em um lock. Não se especifica qual thread vai ser notificada. nofityAll(): Notifica todas as threads que estejam esperando em um lock. 96 96 Quando usar notify()? Acordar uma única thread. Não é possível especificar qual thread é acordada. Útil para sistemas massivamente paralelo: Grande número de threads. Todas fazendo tarefas similares. Então, não importa qual é acordada. 97 Notify, which wakes up a single thread. Because notify doesn't allow you to specify the thread that is woken up, it is useful only in massively parallel applications — that is, programs with a large number of threads, all doing similar chores. In such an application, you don't care which thread gets woken up. 97 Alguns detalhes Notificações: Devem ser usados na implementação das operações dos objetos sincronizados. Evitando Starvation: Certifique que as threads não fiquem em espera eternamente. Notifique-as! 98 Assim como o wait Esses métodos devem ser usados na implementação das operações dos objetos sincronizados, ou seja, nos métodos synchronized dos objetos sincronizados. Pois só faz sentido uma thread notificar ou esperar por outra em objetos sincronizados e o Sistema de Execução Java tem mecanismos para saber qual thread irá esperar ou receber a notificação, pois somente uma thread por vez pode estar executando um método sincronizado de um objeto compartilhado. Don't forget to ensure that the application has other threads which acquire locks on the objects (on which few other threads have previously called wait() method) and call either notify() and notifyAll() methods so that the application can avoid startvation of those threads which have called wait() on the objects under consideration. notifyAll() scheduled all the waiting threads whereas notify()randomly picks one of the waiting threads and schedules it. Use the method which suits the design of your application. Read more aout the differences betweenthe two methods in this article. 98 Exemplo de fluxo de notificação Thread A invoca o método wait(). O lock é liberado. Execução suspensa. Outra thread adquire o mesmo lock. Ela invoca o notifyAll() . Notifica todas as threads que esperam pelo lock. 99 Exemplo de Fluxo 100 Objetos imutáveis O estado do objeto não pode ser alterado após sua construção. São muito úteis em aplicações concorrentes. Não são corrompidos por interferência de threads. São consistentes. 101 An object is considered immutable if its state cannot change after it is constructed. Maximum reliance on immutable objects is widely accepted as a sound strategy for creating simple, reliable code. Immutable objects are particularly useful in concurrent applications. Since they cannot change state, they cannot be corrupted by thread interference or observed in an inconsistent state. Programmers are often reluctant to employ immutable objects, because they worry about the cost of creating a new object as opposed to updating an object in place. The impact of object creation is often overestimated, and can be offset by some of the efficiencies associated with immutable objects. These include decreased overhead due to garbage collection, and the elimination of code needed to protect mutable objects from corruption. The following subsections take a class whose instances are mutable and derives a class with immutable instances from it. In so doing, they give general rules for this kind of conversion and demonstrate some of the advantages of immutable objects. 101 Estratégia de definição de um objeto imutável Não fornecer os métodos “set”. Fazer todos os atributos como final e private. Não permitir subclasses sobrescrever métodos. Declarando a classe como final. Fazer o construtor privado e construir instancias em fábricas de métodos. 102 Don't provide "setter" methods — methods that modify fields or objects referred to by fields. Make all fields final and private. Don't allow subclasses to override methods. The simplest way to do this is to declare the class as final. A more sophisticated approach is to make the constructor private and construct instances in factory methods. 102 Caso algum atributo seja uma instância de um objeto mutável, não permitir que esses objetos sejam mudados: Não fornecer métodos que modifica os objetos mutáveis. Se necessário, crie cópias e armazene as referências para as cópias. Estratégia de definição de um objeto imutável 103 If the instance fields include references to mutable objects, don't allow those objects to be changed: Don't provide methods that modify the mutable objects. Don't share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods. 103 Classe Imutável: Exemplo 104 Exercícios práticos Crie um classe ContadorTempo que possui um atributo tick inteiro e um método nextTick() Crie um classe Relogio que possui um contadorTempo e atraves de uma thread chama o metodo nextTick a cada segundo Crie um classe Cronometro que herda de ContadorTempo e através de uma thread chama o nextTick() Exercícios práticos Implementar os exemplos do link abaixo, inclusive os 3 exercícios propostos ao final. http://docs.oracle.com/javase/tutorial/essential/concurrency/ Referências Java - Como programar, de Harvey M. Deitel, 8ª edição. Use a cabeça! - Java, de Bert Bates e Kathy Sierra. Effective Java Programming Language Guide, de Joshua Bloch. http://docs.oracle.com/javase/tutorial/essential/concurrency/
Compartilhar