Baixe o app para aproveitar ainda mais
Prévia do material em texto
Programação Concorrente Threads em Java Prof. Antonio Felipe Podgorski Bezerra, M.Sc. afbezerra@unicarioca.edu.br Sumário ´ Executar tarefas simultaneamente utilizando Threads. ´ Entender o escalonador de Threads. ´ Problemas com concorrência e sincronismos em Threads. ´ 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. 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 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: 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 Exercícios práticos 4) Crie a classe BarraDeProgresso, que será executada por uma Thread, 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. 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; 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 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. Escalonamento Fonte: Java, Como programar. Deitel, 6ª Edição 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. 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); } } } } 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 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; 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. 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. 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. 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. DÚVIDAS?
Compartilhar