Baixe o app para aproveitar ainda mais
Prévia do material em texto
1 THREADS E CONCORRENCIA EM JAVA – PROF. EDIBERTO Thread é a tarefa que um determinado programa realiza. Fio de execução, também conhecido como linha ou encadeamento de execução, (em inglês: Thread), é uma forma de um processo dividir a si mesmo em duas ou mais tarefas que podem ser executadas concorrencialmente. Porque usar threads? Para se rodar um programa puramente sequencial Exemplos de usos Um exemplo simples seria um jogo, que pode ser modelado com linhas de execução diferentes, sendo uma para desenho de imagem e outra para áudio. Neste caso, há um thread para tratar rotinas de desenho e outro thread para tratar áudio; No ponto de vista do usuário, a imagem é desenhada ao mesmo tempo em que o áudio é emitido pelos alto-falantes; Porém, para sistemas com uma única CPU, cada linha de execução é processada por vez. Vantagens do uso de Threads Simplificação do modelo de programação: Em muitas aplicações ocorrem múltiplas atividades simultaneamente, e algumas delas podem ser bloqueadas de tempos em tempos. Ao decompor uma aplicação em múltiplos threads sequenciais que executam quase em paralelo, há uma simplificação do modelo de programação. Processo Thread Thread Thread Thread Thread Thread Thread Thread 2 Threads são mais rápidos de criar e destruir se comparado aos processos, uma vez que não possuem quaisquer recursos associados a eles. Desempenho: quando há grande quantidade de computação e de E/S, os threads permitem que essas atividades se sobreponham, acelerando a aplicação. Particularidades Cada thread tem o mesmo contexto de software e compartilha o mesmo espaço de memória (endereçado a um mesmo processo-pai), porém o contexto de hardware é diferente. Sendo assim o overhead causado pelo escalonamento de uma thread é muito menor do que o escalonamento de processos. Entretanto, algumas linguagens (C, por exemplo) não fornecem acesso protegido à memória nativa (sua implementação fica a cargo do programador ou de uma biblioteca externa) devido ao compartilhamento do espaço de memória. Um dos benefícios do uso das threads advém do fato do processo poder ser dividido em várias threads; quando uma thread está à espera de determinado dispositivo de entrada/saída ou qualquer outro recurso do sistema, o processo como um todo não fica parado, pois quando uma thread entra no estado de 'bloqueio', uma outra thread aguarda na fila de prontos para executar. Uma thread possui um conjunto de comportamentos padrão, normalmente encontrados em qualquer implementação ou sistema operativo. Uma thread pode: criar outra da mesma forma que um processo, através do método thread- create, onde a thread retorna um ID como primeiro argumento (resultado da função de criação); esperar outra thread se sincronizar, através do método join; voluntariamente "desistir" da CPU por não precisar mais do processamento proposto pela própria ou por vontade do utilizador. Feito através do método thread-yield; replicar-se sem a necessidade de duplicar todo o processo, economizando assim memória, processamento da CPU e aproveitando o contexto (variáveis, descritores, dispositivos de I/O). 3 Threads em Java 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. Exemplo: public class GeraPDF { public void rodar () { // lógica para gerar o pdf... } } public class BarraDeProgresso { public void rodar () { // mostra barra de progresso e vai atualizando ela... } } 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: Exemplo: 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(); } } O código acima, porém, não irá compilar. Como a classe Thread sabe que deve chamar o método 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. CONTRATO 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. 4 Exemplo1: public class GeraPDF implements Runnable { public void run () { // lógica para gerar o pdf... } } Exemplo2: public class BarraDeProgresso implements Runnable { public void run () { // mostra barra de progresso e vai atualizando ela... } } 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. OBS. É o bom uso de interfaces, contratos e polimorfismo na prática! Ciclo de Vida de um Thread Num certo momento, o thread pode estar em um entre vários estados e podemos identificar os estado e como transições ocorrem entre estados como mostra a figura abaixo Esse tipo de diagrama chama-se também "diagrama de estados finito" (porque o número de estados é finito). O diagrama abaixo não está completo, mas mostra as partes essenciais da vida de um thread. O thread está "pronto para rodar" Se a máquina tiver um único processador, só se pode rodar de verdade um thread de cada vez. O sistema Java (JVM) deve implementar um esquema de escalonamento que compartilhe a CPU entre todos os threads que estão no estado "Runnable" 5 Portanto, um thread que está no estado "Runnable" pode, de fato, estar esperando a CPU. O primeiro argumento do construtor de Thread deve ser um objeto que implemente a interface Runnable. Obs. Existe apenas uma forma de se criar explicitamente uma thread em Java: estendendo-se a classe Thread Exemplo: public class Thread01 implements Runnable { ... … } Iniciando um thread - start() A próxima linha (em destaque abaixo) aloca os recursos necessários para executar um thread (pilha, etc.). Exemplo: public void start() { … Thread01.start(); } Executando um thread - run() Depois que start() retorna, o thread está no estado "Runnable". Exemplo: public void run() { Thread01 proc1 = new Thread01(); … Thread thread1 = new Thread(proc1); thread1.start(); } 6 Estado Non-Runnable (PARADO) de um THREAD método sleep() e método wait() Um thread passa a ser "Non-Runnable" quando um dos seguintes eventos ocorre: 1 - O método sleep() é chamado. 2 - O thread chama o método wait() que espera por uma condição. O thread faz um system call "lento" tal como um system call que causa I/O Também fala-se que o thread está "bloqueado" esperando algo. Thread Dormindo Para que a thread atual durma basta chamar o método a seguir, por exemplo, para dormir 3 segundos: Thread.sleep(3 * 1000); Exemplo: Thread thread2 = newThread(proc2); try { thread2.sleep(3*1000); thread2.start(); } catch (InterruptedException e) { thread2.start(); } Quando o thread está dormindo, ele não roda, mesmo que a CPU esteja livre. Para cada forma de entrar no estado Non-Runnable, alguma forma específica fará o thread retornar para o estado Runnable. Ex.: para retornar de sleep, tempo tem que passar. Ex.: para retornar de wait, outro objeto vai ter que fazer a condição ocorrer através de uma chamada a notify(). Ex.: para retornar de I/O lento, o I/O tem que terminar Em relação às threads no Java: - Só se pode usar wait, notify() e notifyAll() quando se está de posse do lock do objeto. - Há três versões do método wait() da classe Object. - wait() espera uma condição para o objeto corrente e esta condição ocorre com notify() no mesmo objeto. 7 - "Ter posse do lock" também se chama "possuir o monitor do objeto". Parando um thread Há método stop() para parar um thread mas ele não deve ser usado, devido está "deprecated" porque descobriu-se que o método tem problemas (é "inseguro"). A outra forma é fazer com que o thread saque que ele tem que parar e retornar do método run(), usando um loop ou o método stop(). Conforme seguem a sintaxe: Loop: public void run() { int i = 0; while (i < 100) { i++; System.out.println("i = " + i); } } método stop(). Thread thread2 = new Thread(proc2); thread2 = null; Multithreading A concorrência de dados é um dos principais problemas a se enfrentar quando empregamos multithreading. Ela é capaz de gerar, desde inconsistência nos dados compartilhados, até erros em tempo de execução. No entanto, felizmente isto pode ser evitado, sendo necessário se precaver para que o aplicativo não apresente tais problemas. Uma boa forma de evitar problemas de concorrência é sincronizar as threads que compartilham dados entre si. A partir disso, estas threads passam a executar em sincronia com outras, e assim, uma por vez acessará o recurso. 8 ESCALONADOR E TROCAS DE CONTEXTO (execução em paralelo) 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. 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. Computadores de mais de um processador: CPU Core 1 CPU Core 0 CPU Core 2 Processo Thread 0 Thread 1 Thread 2 9 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. Exemplo: package control; public class Thread01 implements Runnable { private int codigo; // colocar getter e setter para o atributo codigo public int getCodigo() { return codigo; } public void setCodigo(int codigo) { this.codigo = codigo; } public void run () { for (int i = 0; i < 10; i++) { System.out.println("Processamento " + codigo + " valor: " + i); } } } package model; import control.Thread01; public class ExecutaThread01 { public static void main(String[] args) { Thread01 proc1 = new Thread01(); proc1.setCodigo(1); Thread thread1 = new Thread(proc1); thread1.start(); Thread01 proc2 = new Thread01(); proc2.setCodigo(2); Thread thread2 = new Thread(proc2); thread2.start(); } } 10 SAÍDAS 1ª Execução 2ª Execução 3ª Execução THREAD DORMINDO dormindo 3 segundos Exemplo: package control; public class Thread01 implements Runnable { private int codigo; // colocar getter e setter para o atributo codigo public int getCodigo() { return codigo; } public void setCodigo(int codigo) { this.codigo = codigo; } public void run () { for (int i = 1; i <= 10; i++) { System.out.println("Procesamento " + codigo + " valor: " + i); } } } package model; import control.Thread01; public class ExecutaThread01 { @SuppressWarnings("static-access") public static void main(String[] args) { Thread01 proc1 = new Thread01(); proc1.setCodigo(1); Thread thread1 = new Thread(proc1); thread1.start(); 11 Thread01 proc2 = new Thread01(); proc2.setCodigo(2); Thread thread2 = new Thread(proc2); try { thread2.sleep(3*1000); thread2.start(); } catch (InterruptedException e){ thread2.start(); } } } SAÍDAS DEPOIS DE 3 SEGUNDOS 12 Parando um thread - (thread = null) Exemplo package control; public class Thread01 implements Runnable { private int codigo; // colocar getter e setter para o atributo codigo public int getCodigo() { return codigo; } public void setCodigo(int codigo) { this.codigo = codigo; } public void run () { for (int i = 1; i <= 10; i++) { System.out.println("Procesamento " + codigo + " valor: " + i); } } } package model; import control.Thread01; public class ExecutaThread01 { @SuppressWarnings("static-access") public static void main(String[] args) { Thread01 proc1 = new Thread01(); proc1.setCodigo(1); Thread thread1 = new Thread(proc1); thread1.start();Thread01 proc2 = new Thread01(); proc2.setCodigo(2); Thread thread2 = new Thread(proc2); thread2 = null; } } SAÍDA
Compartilhar