programação orientada a objetos (dpadf 0063)bruno/disciplinas/poo/slides/aula10_threads.pdf ·...
TRANSCRIPT
Programação Orientada a Objetos
(DPADF 0063) Aula 10 – Threads
Universidade Federal de Santa Maria
Colégio Agrícola de Frederico Westphalen
Curso Superior de Tecnologia em Sistemas para Internet
Prof. Bruno B. Boniati – www.cafw.ufsm.br/~bruno
Linhas de execução independentes
Programação Concorrente
• Programar de forma concorrente significa pensar e escrever
programas de computador que fazem mais de uma coisa ao
mesmo tempo;
• Na vida real, muito do que fazemos (respirar, falar, ler, escutar)
e do que os computadores fazem (compilar, imprimir, tocar
música, baixar vídeo) acontece de forma concorrente.
• Os objetivos da programação concorrente podem ser:
▫ Reduzir o tempo total de processamento
▫ Aumentar a confiabilidade disponibilidade das aplicações
▫ Especializar serviços (SOs, simuladores), etc.
Onde utilizar programação concorrente?
• Programação Reativa ▫ A aplicação responde a eventos de entrada (em uma GUI cada
evento corresponde a uma ação).
• Programação Interativa ▫ Escrever um programa dividindo tarefas: uma tarefa para fazer
alguma interação com o usuário, outra para exibir mensagens,
outra para fazer animação, etc...
• Paralelismo físico/distribuição ▫ Permite tirar vantagem de múltiplas CPUs centralizadas ou
distribuídas (cada vez mais presentes nos sistemas
computacionais).
Threads
• Uma thread é uma linha de execução, um fluxo de execução,
um segmento de programa executando dentro da CPU;
• Programação multithreading consiste em pensar e escrever as
aplicações de forma que ações sejam executadas em paralelo
de forma concorrente;
• Apenas computadores com múltiplos processadores podem de
fato executar instruções em paralelo, porém, sistemas
operacionais modernos oferecem recursos para criar uma
“ilusão” de paralelismo em máquinas monoprocessadas
compartilhando o tempo de execução entre diferentes
aplicações.
Threads (cont.)
• A figura abaixo demonstra a utilização da CPU por segmentos
de código. Observe que apenas um segmento ocupa a CPU de
cada vez e há uma fila de segmentos aguardando sua vez ...
Segmento
Segmento
Segmento
Segmento
Segmento
Segmento
Segmento
Segmento
CPU
Fila de
Segmentos
segmento
ganha a CPU
segmento deixa a CPU
Programação concorrente & Java
• Java disponibiliza a concorrência de forma nativa por meio da
linguagem e de seu framework;
• Ao especificar uma thread estamos criando um fluxo de
execução independente, com sua própria pilha de chamadas de
método e contador de programa;
• Java inclui primitivos de multithreading de forma integrada à
linguagem de programação, não sendo necessário chamar
primitivas específicas do sistema operacional (isso é muito
importante para a portabilidade do código).
• A classe Thread representa um fluxo de execução
independente para a linguagem Java.
Um pequeno exemplo ...
• Compile o código abaixo e o execute a classe resultante ...
public class Corrida extends Thread {
public Corrida(String n) {
super(n);
}
public void run() {
for (int i=1; i<=10; i++) {
System.out.println(getName() + " na volta " + i);
}
System.out.println("-->" + getName() + " chegou ");
}
public static void main (String args[]) {
Corrida s = new Corrida("Sebastian Vettel");
Corrida f = new Corrida("Fernando Alonso");
Corrida k = new Corrida("Kimi Raikkonen");
s.start();
f.start();
k.start();
}
}
Quem ganhou a corrida ? (execute novamente ... e agora, quem ganhou?)
O método run()
contém a tarefa que
a thread deve
executar. Quando
ele termina a thread
termina
O método start() coloca a thread
na fila para tentar ganhar CPU
Métodos de uma thread
• getName()
▫ Cada thread pode opcionalmente ser identificada por um nome.
• setPriority(int prioridade)
▫ As threads podem ter prioridades distintas entre si (baixa, média e alta)
• sleep(long tempo)
▫ Faz com que a thread descanse por alguns milissegundos deixando
de ocupar a CPU. Ao acordar ela volta para a fila.
• start()
▫ Coloca a thread na fila para tentar ganhar CPU.
• yield()
▫ Retira a thread da CPU e já a recoloca na fila novamente (isso dá
chance de uma outra thread ganhar a CPU).
Ciclo de vida de uma thread
running
(executando) sleeping
(dormindo)
waiting
(aguardando
algum recurso) runnable
(aguardando
na fila da CPU)
Monitor
Waiting
Dead
(Encerrada)
O que leva a thread deixar a CPU?
• O objetivo de uma thread é alcançar a CPU para que possa ser
executada, mas há situações onde a thread deixa a CPU:
▫ Voluntariamente (através do método yield() para dar chance a outras
threads ocuparem o processador);
▫ Quando ela está aguardando algum recurso de IO (dados de arquivo,
entrada via teclado);
▫ Quando ela está aguardando lock de um recurso compartilhado;
▫ Por preempção (alguns SOs dão uma fatia de tempo para cada thread);
▫ Por prioridade (quando alguma thread de maior prioridade aparece na fila);
▫ Ao terminar sua tarefa (quando o método run() conclui).
Prioridade de uma thread
• O modelo de escalonamento de Threads da JVM (a forma
como uma Thread é elegida para utilizar a CPU) é baseada em
prioridades.
• Existem 3 prioridades que podem ser utilizadas:
▫ Thread.MIN_PRIORITY (prioridade baixa)
▫ Thread.MAX_PRIORITY (prioridade alta)
▫ Thread.NORM_PRIORITY (prioridade normal)
• O método setPriority() é utilizado para indicar qual é a
prioridade da Thread.
Prioridade de uma thread (cont.)
• A JVM elege as Threads de maior prioridade para ocupar a CPU em
detrimento das Threads de menor prioridade.
• Uma Thread de menor prioridade pode será executada quando as
Threads de maior prioridade
▫ terminarem
▫ executarem “yield()”
▫ executarem “sleep()”
▫ ficarem aguardando algum recurso bloqueado
• Se uma Thread de prioridade alta ocupar a CPU por muito tempo a
JVM pode selecionar, ocasionalmente, uma Thread de prioridade
mais baixa, “preemptando” a Thread de mais alta prioridade
Interface Runnable
• Criar uma classe descendente de Thread nem sempre é uma
boa ideia, principalmente em Java, onde cada classe tem uma
única superclasse.
• A classe Thread disponibiliza alguns métodos estáticos que
recebem objetos de classes que implementam uma interface
específica ... Runnable
• A interface Runnable exige basicamente a implementação do método run().
Interface Runnable (cont) class Tarefa implements Runnable {
String nome;
public Tarefa (String n) {
nome = n;
}
public void run() {
for (int i=0; i<10; i++)
System.out.println(nome + " executando tarefa (" + i + ")");
System.out.println(nome + " concluído ");
}
public static void main(String args[]) {
Tarefa t1 = new Tarefa("t1");
Tarefa t2 = new Tarefa("t2");
new Thread(t1).start();
new Thread(t2).start();
}
}
A interface Runnable
exige o método run()
Uma nova Thread é instanciada
passando como parâmetro um objeto de uma classe Runnable
Sincronização (Imagine o seguinte cenário)
• Uma classe conta bancária com métodos para obter e
atualizar o saldo;
public class Conta {
public double saldo = 0;
public Conta(double saldo) {
this.saldo = saldo;
System.out.println("Saldo inicial: R$" + saldo);
}
public double getSaldo() {return saldo;}
public void setSaldo(double saldo) {this.saldo = saldo;}
}
Sincronização (cont.) (Imagine o seguinte cenário)
• Um classe banco com dispositivos para
movimentar as contas;
public class Banco {
public boolean saque(Conta conta, double valor) {
double saldo = conta.getSaldo();
if (saldo < valor) {
System.out.println("Saldo insuficiente para o saque.");
return false;
}
double novoSaldo = saldo - valor;
System.out.println(Thread.currentThread().getName() +
" sacou R$" + valor +
". Saldo após saque: R$" + novoSaldo);
conta.setSaldo(novoSaldo);
return true;
}
}
Sincronização (cont.) (Imagine o seguinte cenário)
• Uma classe cliente de um banco com sua conta corrente,
um valor para saque e a vontade de gastar todo o dinheiro;
public class Cliente extends Thread {
private static Banco banco = new Banco();
private Conta conta = null;
private double valor = 1000;
public Cliente(String nome, Conta conta) {
super(nome);
this.conta = conta;
}
public void run() {
double total = 0;
while (banco.saque(conta,valor))
total += valor;
System.out.println(getName() + " sacou total de R$" + total);
}
}
Sincronização (cont.) (Imagine o seguinte cenário)
• Várias instâncias de clientes (uma família) movimentando
uma mesma conta ao mesmo tempo.
public class Familia {
public static void main (String args[]) {
final Conta conta = new Conta(100000);
Cliente pai = new Cliente("Pai ", conta);
Cliente mae = new Cliente("Mãe ", conta);
Cliente filho = new Cliente("Filho", conta);
pai.start();
mae.start();
filho.start();
}
}
Sincronização (cont.) (Observe a execução da classe família)
Saldo inicial: R$10000.0
Pai sacou R$1000.0. Saldo após saque: R$9000.0
Filho sacou R$1000.0. Saldo após saque: R$9000.0
Mãe sacou R$1000.0. Saldo após saque: R$9000.0
Filho sacou R$1000.0. Saldo após saque: R$8000.0
Pai sacou R$1000.0. Saldo após saque: R$8000.0
Filho sacou R$1000.0. Saldo após saque: R$7000.0
Mãe sacou R$1000.0. Saldo após saque: R$8000.0
Filho sacou R$1000.0. Saldo após saque: R$6000.0
Pai sacou R$1000.0. Saldo após saque: R$7000.0
Filho sacou R$1000.0. Saldo após saque: R$5000.0
Mãe sacou R$1000.0. Saldo após saque: R$7000.0
Filho sacou R$1000.0. Saldo após saque: R$4000.0
Pai sacou R$1000.0. Saldo após saque: R$6000.0
Filho sacou R$1000.0. Saldo após saque: R$3000.0
Mãe sacou R$1000.0. Saldo após saque: R$6000.0
Filho sacou R$1000.0. Saldo após saque: R$2000.0
Pai sacou R$1000.0. Saldo após saque: R$5000.0
Filho sacou R$1000.0. Saldo após saque: R$1000.0
Mãe sacou R$1000.0. Saldo após saque: R$5000.0
Filho sacou R$1000.0. Saldo após saque: R$0.0
Pai sacou R$1000.0. Saldo após saque: R$4000.0
Saldo insuficiente para o saque.
Mãe sacou R$1000.0. Saldo após saque: R$4000.0
Filho sacou total de R$10000.0
Pai sacou R$1000.0. Saldo após saque: R$3000.0
Mãe sacou R$1000.0. Saldo após saque: R$3000.0
Pai sacou R$1000.0. Saldo após saque: R$2000.0
Mãe sacou R$1000.0. Saldo após saque: R$2000.0
Pai sacou R$1000.0. Saldo após saque: R$1000.0
Mãe sacou R$1000.0. Saldo após saque: R$1000.0
Pai sacou R$1000.0. Saldo após saque: R$0.0
Saldo insuficiente para o saque.
Mãe sacou R$1000.0. Saldo após saque: R$0.0
Saldo insuficiente para o saque.
Pai sacou total de R$10000.0
Mãe sacou total de R$10000.0
Saldo inicial R$ 10.000,00
Saldo insuficiente para o saque
Mãe sacou R$ 1000,00 (saldo após o saque R$ 3 mil)
Quanto cada um sacou até que o saldo acabasse?
Filho sacou total de R$ 10000
Pai sacou total de R$ 10000
Mãe sacou total de R$ 10000
Sincronização (cont.)
• O código executado anteriormente utiliza um recurso compartilhado entre
todas as Threads (objeto da classe Conta);
• Para garantir acesso exclusivo a um recurso compartilhado, ou seja, criar
uma fila de espera para utilizar o recurso um de cada vez, faz-se necessário
utilizar recursos de sincronização.
• A palavra reservada synchronized marca um método ou um bloco de
código como sincronizado, isso garante que aquele código somente será
acessado por uma Thread de cada vez.
• Experimente alterar a interface dos métodos abaixo incluindo a palavra synchronized:
Conta.getSaldo()
Conta.setSaldo(double)
Banco.saque(Conta, double)
Exercícios para fixação
Corrida Maluca...
• Codifique uma classe veículo que implementa a interface runnable.
• A cada corrida, cada veículo possui uma velocidade que é sorteada quando a
classe é instanciada.
• Simule uma corrida maluca com os personagens do desenho animado
Wacky Races (produzido nos estúdios Hanna-Barbera nos anos de 68 e 70).
• Utilize todos os personagens (Dick Vigarista & Muttley, Irmãos Rocha,
Cupê Mal-Assombrado, Professor Aéreo, Barão Vermelho, Penélope Charmosa,
Carro Tanque, Quadrilha de Morte, Peter Perfeito e Rufus Lenhador)