Você está na página 1de 22

Sincronizao de Threads

Introduo
Por enquanto, os threads que vimos so independentes y Os threads no requerem acesso a recursos externos ou a chamdas de mtodos de outros objetos y Os threads no precisam de preocupar com o que est ocorrendo com os outros threads
y

Em outras situao, threads devem compartilhar dados e so obrigados a se preocupar com o que os outros esto fazendo Um exemplo disso a aplicao chamada "Produtor/Consumidor" em que um Produtor produ z um fluxo de dados consumidos pelo consumidor y Por exemplo, um thread (o produtor) poderia estar gravando dados num arquivo enquanto outro thread (o consumidor) l dados do mesmo arquivo y Outro exemplo: enquanto voc digita no teclado, o produtor coloca eventos de mouse numa fila de eventos e o consumidor l os eventos da mesma fila y Em ambos os casos, temos threads que compartilham um recurso comum y Com tal compartilhamento, os threads devem se sincronizar para acessar o recurso

O exemplo Produtor/Consumidor
Um produtor gera um nmero entre 0 e 9 (a varivel i) e o armazena num objeto chamado CubbyHole (um lugarzinho para guardar coisas; um cubculo)

Para deixar o exemplo mais interessante, o produtor dorme durante um intervalo aleatrio entre 0 1 100 ms antes de gerar mais nmeros y No programa abaixo, "number" a identificao do produtor, "i" o nmero gerado

public class Producer extends Thread { private CubbyHole cubbyhole; private int number;

public Producer(CubbyHole c, int number) { cubbyhole = c; this.number = number; }

public void run() { for (int i = 0; i < 10; i++) { cubbyhole.put(number, i); try { sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { } } } }

O consumidor consome os inteiros do mesmo CubbyHole asssim que se tornam disponveis

public class Consumer extends Thread { private CubbyHole cubbyhole; private int number;

public Consumer(CubbyHole c, int number) { cubbyhole = c; this.number = number; }

public void run() { int value = 0; for (int i = 0; i < 10; i++) { value = cubbyhole.get(number); } } }
O produtor e o consumidor compartilham dados atravs de um objeto comum da classe CubbyHole y Embora o consumidor deva receber cada nmero exatamente uma vez, nem o produtor nem o consumidor tratam de fazer isso acontecer

A sincronizao entre os dois threads ocorre num nvel mais baixo, dentro do CubbyHole y Veremos detalhes adiante Vamos supor que no houvesse sincronizao entre os threads. Que problemas poderiam ocorrer? Um problema ocorre quando o produtor mais rpido do que o consumidor e gera dois nmeros antes de o consumidor ter chance de consumir o primeiro y Nessa situao, o consumidor perde um nmero:
y

Um outro problema ocorre quando o consumidor mais rpido do que o produtor y Um nmero pode ser consumido duas (ou mais)vezes:

Em ambos os casos, o resultado no o que se deseja: o consumidor deve receber cada nmero exatamente uma vez Um problema desse tipo se chama uma condio de corrida y Uma condio de corrida uma situao em que 2 ou mais threads ou processos esto compartilhando dados (lendo ou gravando) e o resultado final depende do "timing" do escalonamento dos threads y Condies de corrida levam a resultados imprevisveis e a bugs sutis, difceis de achar y Um bug fcil de remover se for reproduzvel; quando no o , o mundo se torna cruel ... Uma soluo fazer a sincronizao dentro do CubbyHole

O CubbyHole s deve permitir o armazenamento de outro nmero quando o anterior tiver sido consumido As atividades do produtor e do consumidor devem ser sincronizados de duas formas diferentes y Primeiro, os threads no devem acessar o CubbyHole simultaneamente y Um thread pode impedir isso travando o objeto (locking) y Quando um objeto est travado, um outro thread que chamar um mtodo sincronizado no mesmo objeto vai bloquear at o obvjeto ser destravado y Segundo, os threads devem coordenar seu trabalho y O produtor deve ter uma forma de dizer ao consumidor que um novo nmero est disponvel para consumo y O consumidor deve ter uma forma de dizer ao produtor que o nmero foi consumido, liberando a produo de outro nmero y A classe Object prov mtodos (wait, notify, notifyAll) para permitir que threads esperem por uma condio e notificar outros threads quando uma condio ocorre
y

A primeira sincronizao: travando um objeto (locking)


Dentro de um programa, segmentos de cdigo que acessam os mesmos dados usando threads diferentes e concorrentes so chamados de regies crticas ou sees crticas Uma seo crtica pode ser um bloco de statements ou um mtodo inteiro e deve ser identificada com a palavra synchronized A JVM fornece um lock para cada objeto e o objeto travado (o lock "obtido") ao entrar numa seo crtica No exemplo acima, os mtodos put e get de CubbyHole.java so as regies crticas y Os dados compartilhados so os atributos de CubbyHole

Os threads devem acessar tais dados se forma sequencial Eis o cdigo com a indicao da regies crticas:

public class CubbyHole { private int contents; private boolean available = false;

public synchronized int get(int who) { ... }

public synchronized void put(int who, int value) { ... } }


Ao entrar num mtodo synchronized, o thread que chamou o mtodo trava o objeto cujo mtodo foi chamado y Observe que um thread trava um objeto y Se outro thread chamar um mtodo synchronized do mesmo objeto, ele vai esperar at o primeiro liberar o lock A aquisio e liberao de um lock feita de forma atmica pela JVM y Time-slicing ou outras formas de escalonar threads no pode causar furos ao esquema A palavra synchronized pode se aplicar a um bloco:

// ... synchronized { // qualquer conjunto de statements }


Tambm possvel adquirir o lock de outro objeto, no s de this:

// ... synchronized(obj) { // qualquer conjunto de statements }


syncronized sozinho equivalente a synchronized(this)

Readquirindo um lock
O mesmo thread pode obter um lock que ele j possui sem ter que esperar y Seno teramos "deadlock", uma condio de espera permanente Veja o exemplo:

public class Reentrant { public synchronized void a() { b(); System.out.println("here I am, in a()"); } public synchronized void b() {

System.out.println("here I am, in b()"); } }


Dizemos que locks so "reentrantes" A sada :

here I am, in b() here I am, in a()

A segunda sincronizao: usando os mtodos notifyAll e wait


Vamos investigar como o cdigo de CubbyHole permite que o produtor e o consumidor sincronizem suas atividades O CubbyHole guarda o valor num atributo privado chamado "contents" H um outro atributo privado chamado "available", um booleano que indica se h um valor presente no objeto y Isto , um valor produzido mas ainda no consumido Examine essa primeira tentativa de implementao de CubbyHole:

public synchronized int get() { if (available == true) { available = false; return contents; } }

//won't work!

public synchronized void put(int value) { //won't work! if (available == false) { available = true; contents = value; } }
Isso no funciona (na realidade, nem compila!) y Exemplo de furo: se no houver nada no CubbyHole (available == false), o mtodo get() faz nada y O correto seria esperar at ter algo y Outro exemplo de furo: se houver algo no CubbyHole (available == true), o mtodo put() faz nada e perde o valor y O correto seria esperar at poder guardar o valor Temos que dar um jeito de haver espera e de um thread sinalizar o outro quando pode continuar Isso feito com os mtodos wait e notifyAll:

public synchronized int get() { while (available == false) { try { //wait for Producer to put value wait(); } catch (InterruptedException e) { } } available = false;

//notify Producer that value has been retrieved notifyAll(); return contents; }

public synchronized void put(int value) { while (available == true) { try { //wait for Consumer to get value wait(); } catch (InterruptedException e) { } } contents = value; available = true; //notify Consumer that value has been set notifyAll(); }
O mtodo wait libera o lock e espera notificao para continuar y Isso necessrio para que o outro thread possa adquirir o lock, fazer seu trabalho e acordar o outro (com notifyAll) y Ao continuar, o lock obtido novamente O mtodo notifyAll "acorda" todos os threads que esto em wait (nesse objeto) y Os threads que acordam competem pelo lock

Um thread pega o lock, os outros voltam a dormir O mtodo notify tambm existe e acorda apenas um thread (escolhido arbitrariamente) Observaes importantes: y S se pode usar wait, notify() e notifyAll() quando se est de posse do lock do objeto (dentro de um bloco synchronized) y wait() espera uma condio para o objeto corrente e esta condio ocorre com notify() no mesmo objeto "Ter posse do lock" tambm se chama "possuir o monitor do objeto" y Detalhes e razes pelo nome Monitor sero dadas na disciplina Sistemas Operacionais H trs verses do mtodo wait da classe Object: y wait() y Espera indefinidamente por uma notificao y wait(long timeout) y Espera por uma notificao mas, depois de timeout milisegundos, volta mesmo sem notificao y wait(long timeout, int nanos) y Como antes, mas com mais resoluo de tempo Esses mtodos podem ser usados em vez de sleep, quando se quer ter um controle maior sobre o que est ocorrendo y Exemplo: sleep no pode ser interrompido mas com wait/notify, podemos ter melhor controle

Executando o exemplo Produtor/Consumidor


Eis um exemplo pequeno do produtor/consumidor (ProducerConsumerTest):

public class ProducerConsumerTest { public static void main(String[] args) { CubbyHole c = new CubbyHole();

Producer p1 = new Producer(c, 1); Consumer c1 = new Consumer(c, 1);

p1.start(); c1.start(); } }
Eis a sada:

Producer #1 put: 0 Consumer #1 got: 0 Producer #1 put: 1 Consumer #1 got: 1 Producer #1 put: 2 Consumer #1 got: 2 Producer #1 put: 3 Consumer #1 got: 3 Producer #1 put: 4 Consumer #1 got: 4 Producer #1 put: 5 Consumer #1 got: 5 Producer #1 put: 6

Consumer #1 got: 6 Producer #1 put: 7 Consumer #1 got: 7 Producer #1 put: 8 Consumer #1 got: 8 Producer #1 put: 9 Consumer #1 got: 9

Locks explcitos e variveis condicionais


Para proteger regies crticas (proteger uma seo de cdigo), podemos tambm usar um lock explcito y Um lock explcito mais flexvel do que usar a palavra synchronized y Permite proteger alguns statements (sem bloco) y permite proteger mltiplos mtodos Para criar um lock explcito, voc instancia uma implementao da interface Lock y Normalmente, instancia-se ReentrantLock Para obter o lock, usa-se o mtodo lock() Para liberar o lock: usa-se unlock() J que o lock no liberado automaticamente no final de um mtodo, pode ser til usar try/finally para garantir que o lock seja liberado Para esperar por um lock explcito, cria-se uma varivel condicional y Um objeto que implementa a interface Condition y Usar Lock.newCondition() para criar uma condio A condio prov mtodos: y "await" para esperar at a condio ser verdadeira

"signal" e "signalAll" para avisar os threads que a condio ocorreu As variantes de "await" aparecem na tabela seguinte
Mtodos Condition.await

Mtodo
await awaitUninterruptibly

Descrio Espera uma condio ocorrer Espera uma condio ocorrer. No pode ser interrompido. Espera uma condio ocorrer. Espera no mximo timeout nanossegundos Espera uma condio ocorrer. Espera no mximo timeout TimeUnit Espera uma condio ocorrer. Espera no mximo at a data especificada

awaitNanos(long timeout)

await(long timeout, TimeUnit unit)

await(Date timeout)

O exemplo abaixo CubbyHole reescrito para usar um lock explcito e uma varivel condicional y Para rodar, execute ProducerConsumerTest2

import java.util.concurrent.locks.*; public class CubbyHole2 { private int contents; private boolean available = false; private Lock aLock = new ReentrantLock(); private Condition condVar = aLock.newCondition();

public int get(int who) {

aLock.lock(); try { while (available == false) { try { condVar.await(); } catch (InterruptedException e) { } } available = false; System.out.println("Consumer " + who + " got: " + contents); condVar.signalAll(); } finally { aLock.unlock(); return contents; } }

public void put(int who, int value) { aLock.lock(); try {

while (available == true) { try { condVar.await(); } catch (InterruptedException e) { } } contents = value; available = true; System.out.println("Producer " + who + " put: " + contents); condVar.signalAll(); } finally { aLock.unlock(); } } }

Estruturas de dados sincronizadas


Em vez de construir sua prpria estrutura de dados sincronizada (como CubbyHole), voc pode usar estruturas j prontas, com detalhes de sincronizao escondidos y O package java.util.concurrent tem tais estruturas O exemplo que segue usa BlockingQueue como cubbyhole y A fila trata de todos os detalhes (proteo de regies crticas, sincronizao par indicar disponibilidade de dados)

Fonte: Producer3.java, Consumer3.java, ProducerConsumerTest3.java

import java.util.concurrent.*; public class Producer3 extends Thread { private BlockingQueue cubbyhole; private int number;

public Producer3(BlockingQueue c, int num) { cubbyhole = c; number = num; }

public void run() { for (int i = 0; i < 10; i++) { try { cubbyhole.put(i); System.out.println("Producer #" + number + " put: " + i); sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { } } }

}
O consumidor semelhante Para rodar o programa, use ProducerConsumerTest3, que cria um ArrayBlockingQueue (implementao de BlockingQueue):

import java.util.concurrent.*; public class ProducerConsumerTest3 { public static void main(String[] args) {

BlockingQueue c = new ArrayBlockingQueue(1); Producer3 p1 = new Producer3(c, 1); Consumer3 c1 = new Consumer3(c, 1);

p1.start(); c1.start(); } }

Sincronizando colees
As colees de Java (ArrayList, ...) no so sincronizadas y Dizemos que o uso dessas colees no seja "thread-safe" y A exceo Vector que sincronizada por motivos histricos Para criar colees sincronizadas, voc pode criar um decorador da coleo que sincroniza os mtodos Java j fornece tais decoradores na classe Collections

H vrias decoradores porque h vrias interfaces para usar colees y Collection y List y Map y Set

Exemplo: como usar uma lista sincronizada

List list = Collections.synchronizedList(new ArrayList());


importante que apenas a nova lista seja utilizada a partir desse ponto y A lista original no deve ser tocada diretamente H um problema especial no uso de um iterator y Ao usar um iterator, temos que "entrar" e "sair" do iterator usando dois mtodos (hasNext e next) y Se cada um desses mtodos fosse sincronized, ainda no teramos uma soluo, pois o que o next retorna tem que corresponder a o que hasNext falou antes y Portanto, temos que fazer uma sincronizao atmica com os dois y Ver abaixo a forma correta de iterar na lista

... synchronized(list) { Iterator i = list.iterator(); // Must be in synchronized block while (i.hasNext()) foo(i.next()); }

Os vrios mtodos disponveis para criar esses decoradores so: y synchronizedCollection y synchronizedList y synchronizedMap y synchronizedSet y synchronizedSortedMap y synchronizedSortedSet

Starvation (morte por inanio) e deadlock


Ao escrever um programa envolvendo vrios threads que competem por recursos, temos que assegurar a equidade y Equidade existe quando cada thread recebe recursos suficientes para progredir de forma razovel y Um sistema equanime evita duas coisas: starvation e deadlock Starvation ocorre quando um ou mais threads no conseguem obter recursos no sistema e no pode progredir Deadlock uma forma especialmente drstica de starvation em que dois ou mais threads esto esperando por uma condio que nunca vai ocorrer y Tipicamente, os threads esto esperando por algum lock ou condio que outro thread possui, e h dependncia circular Vamos exemplificar deadlock com o exemplo do Jantar dos Filsofos (dining philosophers) y Cinco filsofos esto sentados numa mesa redonda y Na frente de cada filsofo, h uma tijela de arroz y Entre cada filsofo h um chopstick (pauzinho chins) y Para poder comer um bocado de arroz, um filsofo deve ter 2 pauzinhos: o sua esquerda e o sua direita y Os filsofos devem achar uma forma de compartilhar pauzinhos de forma a que todos possam comer O applet abaixo implementa o seguinte algoritmo

Um filsofo sempre pega o pauzinho da esquerda primeiro; se o pauzinho estiver a, o filsofo o pega e levanta sua mo esquerda O filsofo tenta pegar o pauzinho da direita, se estiver disponvel, ele o pega e levanta sua mo direita Quando o filsofo tem ambos os pauzinhos, ele come o bocado de arroz e fala "Gostoso!" Ele ento larga os dois pauzinhos, deixando que outros filsofos os peguem O filsofo pausa um pouco e tenta tudo novamente

O slider no applet controla o tempo de espera (a gula!) dos filsofos Quando o slider est em 0, os filsofos no esperam e um deadlock ocorre logo (com a mo esquerda de todos os filsofos para cima) y O deadlock ocorre porque cada um est esperando do algo que nunca ocorrer Com um perodo menor, o deadlock pode demorar um bom tempo at ocorrer Criar programas com deadlock comum e o estudo detalhado de como tratar o assunto ser coberto em outra disciplina (sistemas operacionais) Por enquanto, basta dizer que uma das tcnicas possveis escrever o programa de tal forma a impedir a ocorrncia do deadlock Uma forma de fazer isso de impor uma ordem na variveis condicionais y No exemplo acima, todos os pauzinhos so iguais e no h ordem imposta

y y
programa

Mas podemos numerar os pauzinhos de 1 at 5 e obrigar cada filsofo a pegar o pauzinho de menor nmero primeiro Isso evita qualquer deadlock Detalhes em outra disciplina ...

Você também pode gostar