05 - sincronização de threads - i

Post on 09-Feb-2017

1.056 Views

Category:

Software

1 Downloads

Preview:

Click to see full reader

TRANSCRIPT

UNIVERSIDADE ESTADUAL DO SUDOESTE DA BAHIA CURSO DE CIÊNCIA DA COMPUTAÇÃO

PROGRAMAÇÃO CONCORRENTE – 2015.1

Fábio M. Pereira

(fabio.mpereira@uesb.edu.br)

Roteiro

• Sincronizando um Método

• Atributos Independentes em Classes Sincronizadas

• Usando Condições em Código Sincronizado

• Sincronizando um Bloco de Código com um Bloqueio

• Sincronizando Acesso a Dados com Bloqueios de Leitura/Escrita

• Modificando o Equilíbrio entre Bloqueios

• Usando Múltiplas Condições em um Bloqueio

Introdução

• Uma das situações mais comuns em programação concorrente ocorre quando mais de uma thread de execução compartilha um recurso

• Em um aplicativo concorrente, é normal que várias threads leiam ou escrevam os mesmos dados ou tenham acesso ao mesmo arquivo ou conexão com o banco de dados

• Esses recursos compartilhados podem provocar situações de erro ou inconsistência de dados e temos que implementar mecanismos para evitar esses erros

• A solução para estes problemas é fornecida com o conceito de seção crítica

• A seção crítica é um bloco de código que acessa um recurso compartilhado e não pode ser executado por mais de uma thread, ao mesmo tempo.

Introdução

• Para ajudar os programadores a implementar seções críticas, Java (e quase todas as linguagens de programação) oferece mecanismos de sincronização

• Quando uma thread quer acesso a uma seção crítica, ela usa um desses mecanismos de sincronização para saber se há alguma outra thread em execução na seção crítica

• Se não há, a thread entra na seção crítica, caso contrário, a thread é suspensa pelo mecanismo de sincronização até que a thread que está executando a seção crítica termine

• Quando mais de uma thread está aguardando uma outra thread terminar a execução de uma seção crítica, a JVM escolhe uma delas, e o restante esperar pela sua vez

Introdução

• Esta aula apresenta uma série de exemplos que mostram como usar os dois mecanismos de sincronização básicos oferecidos pela linguagem Java:

– A palavra-chave synchronized

– A interface Lock e suas implementações

Sincronizando um Método

• Veremos como usar um dos métodos mais básicos para a sincronização em Java, isto é, o uso da palavra-chave synchronized para controlar o acesso simultâneo a um método

• Apenas uma thread de execução irá acessar um dos métodos de um objeto declarado com a palavra-chave synchronized

• Se outra thread tenta acessar qualquer método declarado com a palavra-chave synchronized do mesmo objeto, ele será suspenso até que a primeira thread termine a execução do método

• Em outras palavras, cada método declarado com a palavra-chave synchronized é uma seção crítica e Java só permite a execução de uma das seções críticas de um objeto

Sincronizando um Método

• Os métodos estáticos possuem um comportamento diferente: apenas uma thread de execução irá acessar um dos métodos estáticos declarados com a palavra-chave synchronized, mas outra thread pode acessar outros métodos não-estáticos de um objeto dessa classe

• Temos que ter muito cuidado com este ponto, porque duas threads podem acessar dois métodos synchronized diferentes se um é estático e o outro não é

• Se ambos os métodos alterar os mesmos dados, podemos ter erros de inconsistência de dados

Sincronizando um Método

• Para aprender este conceito, vamos implementar um exemplo com duas threads acessando um objeto comum

• Vamos ter uma conta bancária e duas threads: uma que transfere dinheiro para a conta e outra que retira dinheiro da conta

– Sem métodos de sincronização, poderíamos ter resultados incorretos

– Mecanismos de sincronização garantem que o saldo final da conta esteja correto

Programa Exemplo

1. Crie uma classe chamada Account que irá modelar nossa conta bancária. Ela possui um único atributo double, chamado balance (saldo). public class Account {

private double balance;

2. Implemente os métodos setBalance() e getBalance() para escrever e ler o valor do atributo. public double getBalance() {

return balance;

}

public void setBalance(double balance) {

this.balance = balance;

}

Programa Exemplo

3. Implementar um método chamado addAmount() que aumenta o valor do saldo em uma certa quantidade que é passada para o método. Apenas uma thread deve alterar o valor do saldo, portanto, use a palavra-chave synchronized para converter este método em uma seção crítica. public synchronized void addAmount(double

amount) {

double tmp=balance;

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

tmp+=amount;

balance=tmp;

}

Programa Exemplo

4. Implementar um método chamado subtractAmount() que diminui o valor do saldo em uma certa quantidade que é passada para o método. Apenas uma thread deve alterar o valor do saldo, portanto, use a palavra-chave synchronized para converter este método em uma seção crítica. public synchronized void subtractAmount(double

amount) {

double tmp=balance;

try {

Thread.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

tmp-=amount;

balance=tmp;

}

Programa Exemplo

5. Implemente uma classe que simula um caixa eletrônico (ATM). Ele vai usar o método subtractAmount() para diminuir o saldo de uma conta. Esta classe deve implementar a interface Runnable a ser executada como uma thread. public class Bank implements Runnable {

6. Adicione um objeto Account a esta classe. Implemente o construtor da classe que inicializa o objeto Account. private Account account;

public Bank(Account account) {

this.account=account;

}

Programa Exemplo

7. Implemente o método run(). Ele faz 100 chamadas ao método subtractAmount() de uma conta para reduzir o saldo. @Override

public void run() {

for (int i=0; i<100; i++){

account.subtractAmount(1000);

}

}

8. Implementar uma classe que simula uma empresa e utiliza o método addAmount() da classe Account para incrementar o saldo da conta. Esta classe deve implementar a interface Runnable a ser executada como uma thread. public class Company implements Runnable {

Programa Exemplo

9. Adicione um objeto Account a esta classe. Implemente o construtor da classe que inicializa o objeto account. private Account account;

public Company(Account account) {

this.account=account;

}

10. Implemente o método run(). Ele faz 100 chamadas ao método addAmount() de uma conta para incrementar o saldo. @Override

public void run() {

for (int i=0; i<100; i++){

account.addAmount(1000);

}

}

Programa Exemplo

11. Implementar a classe principal do aplicativo criando uma classe principal que contém o método main(). public class Main {

public static void main(String[] args) {

12. Crie um objeto Account e inicialize o seu saldo com 1000. Account account=new Account();

account.setBalance(1000);

13. Crie um objeto Company e uma Thread para executá-lo. Company company=new Company(account);

Thread companyThread=new Thread(company);

Programa Exemplo

14. Crie um objeto Bank e uma Thread para executá-lo. Bank bank=new Bank(account);

Thread bankThread=new Thread(bank);

15. Escreva o saldo inicial no console. Inicie as threads. System.out.printf("Conta : Saldo Inicial:

%f\n",account.getBalance());

companyThread.start();

bankThread.start();

Programa Exemplo

16. Aguarde a finalização das duas threads usando o método join() e imprima no console o saldo final da conta. try {

companyThread.join();

bankThread.join();

System.out.printf("Conta : Saldo Final:

%f\n",account.getBalance());

} catch (InterruptedException e) {

e.printStackTrace();

}

Funcionamento

• Neste exemplo, desenvolvemos um aplicativo que incrementa e diminui o saldo de uma classe que simula uma conta bancária

• O programa faz 100 chamadas ao método addAmount() que incrementa o saldo em 1.000 em cada chamada e 100 chamadas ao método subtractAmount() que diminui o saldo em 1.000 em cada chamada

• Devemos esperar que os saldos inicial e final sejam iguais

Funcionamento

• Tentamos forçar uma situação de erro usando uma variável chamada tmp para armazenar o valor do saldo da conta, de modo que lemos o saldo da conta, incrementamos o valor da variável temporária e então estabelecemos o valor do saldo da conta novamente

• Além disso, introduzimos um pouco de atraso, utilizando o método sleep() da classe Thread para colocar a thread que está executando o método para dormir por 10 milissegundos, portanto, se outra thread executar esse método, ela pode modificar o equilíbrio da conta, provocando um erro

• É o mecanismo da palavra-chave synchronized que evita esses erros

Funcionamento

• Se quisermos ver os problemas de acesso concorrente aos dados compartilhados, basta apagarmos a palavra-chave synchronized dos métodos addAmount() e subtractAmount() e executarmos o programa

• Sem a palavra-chave synchronized, enquanto uma thread está dormindo após a leitura do valor do saldo da conta, outro método irá ler o saldo da conta, então ambos os métodos irão modificar o mesmo saldo e uma das operações não será refletida no resultado final

Funcionamento

• Como podemos ver na captura de tela seguinte, podemos obter resultados inconsistentes:

Funcionamento

• Se executarmos o programa várias vezes, obteremos resultados diferentes

• A ordem de execução das threads não é garantida pela JVM, então, toda vez que executarmos o programa, as threads vão ler e modificar o saldo da conta em uma ordem diferente, de modo que o resultado final será diferente

• Agora, adicione a palavra-chave synchronized como vimos antes e execute o programa novamente

Funcionamento

• Como podemos ver na imagem seguinte, agora obtemos o resultado esperado

• Se você executar o programa várias vezes, iremos obter o mesmo resultado

• Veja a imagem a seguir:

Funcionamento

• Usando a palavra-chave synchronized, nós garantimos o acesso correto para os dados compartilhados em aplicações concorrentes

• Como mencionado na introdução, apenas uma thread pode acessar os métodos de um objeto que use a palavra-chave synchronized na sua declaração

• Se uma thread (A) está executando um método synchronized e outra thread (B), quer executar outro métodos synchronized do mesmo objeto, ela vai ser bloqueada até que a thread (A) termine

• Mas se threadB tem acesso a diferentes objetos da mesma classe, nenhum deles será bloqueado

Sincronizando um Método

• A palavra-chave synchronized penaliza o desempenho da aplicação, portanto, devemos usá-la apenas sobre os métodos que modificam os dados compartilhados em um ambiente concorrente

• Se temos várias threads que chamam um método synchronized, apenas um irá executá-los em um momento enquanto os outros vão estar à espera

• Se essa operação não utilizar a palavra-chave synchronized, todos as threads podem executar a operação, ao mesmo tempo, reduzindo o tempo total de execução

• Se sabemos que um método não será chamado por mais de uma thread, não devemos usar a palavra-chave synchronized

Sincronizando um Método

• Podemos usar chamadas recursivas com métodos synchronized

• À medida que a thread tem acesso aos métodos synchronized de um objeto, podemos chamar outros métodos synchronized desse objeto, incluindo o método que está sendo executado, ela não terá que conseguir acesso aos métodos synchronized novamente

Sincronizando um Método

• Podemos usar a palavra-chave synchronized para proteger o acesso a um bloco de código em vez de um método inteiro

• Devemos utilizar a palavra-chave synchronized desta forma para proteger o acesso aos dados compartilhados, deixando o restante das operações de fora do bloco, obtendo-se um melhor desempenho da aplicação synchronized (this) {

// Java code

}

Sincronizando um Método

• O objetivo é fazer com que a seção crítica (o bloco de código que só pode ser acessado por uma thread por vez) seja tão curta quanto possível

• Temos usado a palavra-chave synchronized para proteger o acesso à instrução que atualiza o número de pessoas no prédio, deixando de fora as longas operações deste bloco que não usam os dados compartilhados

• Quando usamos a palavra-chave synchronized dessa maneira, devemos passar uma referência de objeto como um parâmetro

• Apenas uma thread pode acessar o código synchronized (blocos ou métodos) desse objeto

• Normalmente, vamos usar a palavra-chave this para referenciar o objeto que está executando o método

Atributos Independentes em Classes Sincronizadas

• Quando usamos a palavra-chave synchronized para proteger um bloco de código, devemos passar uma referência de objeto como um parâmetro

• Normalmente, iremos usar a palavra-chave this para referenciar o objeto que executa o método, mas podemos usar outras referências de objeto

• Normalmente, esses objetos serão criados exclusivamente com esta finalidade

• Por exemplo, se temos dois atributos independentes em uma classe compartilhada por várias threads, devemos sincronizar o acesso a cada variável, mas não há nenhum problema se houver uma thread acessando um dos atributos e outra thread acessando o outro, ao mesmo tempo

Atributos Independentes em Classes Sincronizadas

• Neste exemplo, iremos aprender como resolver esta situação com um programa que simula um cinema com duas telas e duas bilheterias

• Quando uma bilheteria vende bilhetes, eles são para um dos dois filmes, mas não para ambos, então o número de assentos livres em cada sala são atributos independentes

Programa Exemplo

1. Crie uma classe chamada Cinema e adicione dois atributos long chamados vacanciesCinema1 e vacanciesCinema2. public class Cinema {

private long vacanciesCinema1;

private long vacanciesCinema2;

2. Adicione à classe Cinema dois atributos Object

adicionais chamados controlCinema1 e controlCinema2. private final Object controlCinema1,

controlCinema2;

Programa Exemplo

3. Implemente o construtor da classe Cinema que inicializa todos os atributos da classe. public Cinema(){

controlCinema1=new Object();

controlCinema2=new Object();

vacanciesCinema1=20;

vacanciesCinema2=20;

}

Programa Exemplo

4. Implemente o método sellTickets1() que é chamado quando alguns bilhetes para o primeiro cinema são vendidos. Ele utiliza o objeto controlCinema1 para controlar o acesso ao bloco de código synchronized. public boolean sellTickets1 (int number) {

synchronized (controlCinema1) {

if (number<vacanciesCinema1) {

vacanciesCinema1-=number;

return true;

} else {

return false;

}

}

}

Programa Exemplo

5. Implemente o método sellTickets2() que é chamado quando alguns bilhetes para o segundo cinema são vendidos. Ele utiliza o objeto controlCinema2 para controlar o acesso ao bloco de código synchronized. public boolean sellTickets2 (int number) {

synchronized (controlCinema2) {

if (number<vacanciesCinema2) {

vacanciesCinema2-=number;

return true;

} else {

return false;

}

}

}

Programa Exemplo

6. Implemente o método returnTickets1() que é chamado quando alguns bilhetes para o primeiro cinema são devolvidos. Ele utiliza o objeto controlCinema1 para controlar o acesso ao bloco de código synchronized. public boolean returnTickets1 (int number) {

synchronized (controlCinema1) {

vacanciesCinema1+=number;

return true;

}

}

Programa Exemplo

7. Implemente o método returnTickets2() que é chamado quando alguns bilhetes para o segundo cinema são devolvidos. Ele utiliza o objeto controlCinema2 para controlar o acesso ao bloco de código synchronized. public boolean returnTickets2 (int number) {

synchronized (controlCinema2) {

vacanciesCinema2+=number;

return true;

}

}

Programa Exemplo

8. Implemente outros dois métodos que retornam o número de vagas em cada cinema. public long getVacanciesCinema1() {

return vacanciesCinema1;

}

public long getVacanciesCinema2() {

return vacanciesCinema2;

}

9. Implemente a classe TicketOffice1 e especifique que ela implementa a interface Runnable. public class TicketOffice1

implements Runnable {

Programa Exemplo

10. Declare um objeto Cinema e implemente o construtor da classe que inicializa este objeto. private Cinema cinema;

public TicketOffice1 (Cinema cinema) {

this.cinema=cinema;

}

Programa Exemplo

11. Implemente o método run() que simula algumas operações sobre os dois cinemas. @Override

public void run() {

cinema.sellTickets1(3);

cinema.sellTickets1(2);

cinema.sellTickets2(2);

cinema.returnTickets1(3);

cinema.sellTickets1(5);

cinema.sellTickets2(2);

cinema.sellTickets2(2);

cinema.sellTickets2(2);

}

Programa Exemplo

12. Implemente a classe TicketOffice2 e especifique que ela implementa a interface Runnable. public class TicketOffice2

implements Runnable {

13. Declare um objeto Cinema e implemente o construtor da classe que inicializa este objeto. private Cinema cinema;

public TicketOffice2 (Cinema cinema) {

this.cinema=cinema;

}

Programa Exemplo

14. Implemente o método run() que simula algumas operações sobre os dois cinemas. @Override

public void run() {

cinema.sellTickets2(2);

cinema.sellTickets2(4);

cinema.sellTickets1(2);

cinema.sellTickets1(1);

cinema.returnTickets2(2);

cinema.sellTickets1(3);

cinema.sellTickets2(2);

cinema.sellTickets1(2);

}

Programa Exemplo

15. Implemente a classe principal do exemplo através da criação de uma classe principal e adicionando-lhe o método main(). public class Main {

public static void main(String[] args) {

16. Declare e crie um objeto Cinema. Cinema cinema=new Cinema();

17. Crie um objeto TicketOffice1 e a Thread para executá-lo. TicketOffice1 ticketOffice1=new

TicketOffice1(cinema);

Thread thread1=new

Thread(ticketOffice1,

"TicketOffice1");

Programa Exemplo

18. Crie um objeto TicketOffice2 e a Thread para executá-lo. TicketOffice2 ticketOffice2=new

TicketOffice2(cinema);

Thread thread2=new

Thread(ticketOffice2,

"TicketOffice2");

19. Inicie ambas as threads. thread1.start();

thread2.start();

Programa Exemplo

20. Aguarde até que as threads sejam finalizadas. try {

thread1.join();

thread2.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

21. Escreva no console as vagas disponíveis nos dois cinemas. System.out.printf("Sala 1 Vagas: %d\n",

cinema.getVacanciesCinema1());

System.out.printf("Sala 2 Vagas: %d\n",

cinema.getVacanciesCinema2());

Funcionamento

• Quando usamos a palavra-chave synchronized para proteger um bloco de código, podemos usar um objeto como um parâmetro

• A JVM garante que apenas uma thread pode ter acesso a todos os blocos de código protegidas com esse objeto (note que sempre falamos sobre objetos, e não sobre classes)

Funcionamento

• Neste exemplo, temos um objeto que controla o acesso ao atributo vacanciesCinema1, portanto, apenas uma thread pode modificar este atributo de cada vez, e outro objeto que controla o acesso ao atributo vacanciesCinema2, portanto, apenas uma thread pode modificar este atributo de cada vez

• Mas podem haver duas threads em execução concorrente, uma modificando o atributo vacancesCinema1 e a outra modificando o atributo vacanciesCinema2

Funcionamento

• Quando executarmos esse exemplo, podemos ver como o resultado final é sempre o número esperado de vagas para cada cinema

• Na captura de tela seguinte, podemos ver os resultados de uma execução da aplicação:

Usando Condições em Código Sincronizado

• Um problema clássico na programação concorrente é o problema do produtor-consumidor

• Temos um buffer de dados, um ou mais produtores de dados que os salvam no buffer e um ou mais consumidores de dados que leem a partir do buffer

• À medida que o buffer é uma estrutura de dados compartilhada, temos que controlar o acesso a ela através de um mecanismo de sincronização como o da palavra-chave synchronized, mas que tenha mais limitações

• Um produtor não pode salvar dados no buffer se ele está cheio e que o consumidor não pode retirar dados do buffer se ele está vazio

Usando Condições em Código Sincronizado

• Para esses tipos de situações, Java fornece os métodos wait(), notify() e notifyAll() implementado na classe Object

• Uma thread pode chamar o método wait() dentro de um bloco de código synchronized – Se ela chamar o método wait() do lado de fora de um bloco de

código synchronized, a JVM lança uma exceção IllegalMonitorStateException

• Quando a thread chama o método wait(), a JVM coloca a thread para dormir e libera o objeto que controla o bloco de código synchronized que está em execução e permite que as outras threads executem outros blocos de código synchronized protegidos por esse objeto

• Para acordar a thread, devemos chamar o método notify() ou notifyAll() dentro de um bloco de código protegido pelo mesmo objeto

Usando Condições em Código Sincronizado

• Neste exemplo, vamos aprender como implementar o problema do produtor-consumidor, utilizando a palavra-chave synchronized e os métodos wait(), notify(), e notifyAll()

Programa Exemplo

1. Crie uma classe chamada EventStorage. Ela tem dois atributos: um atributo int chamado maxSize e um atributo LinkedList <Date> chamado storage. public class EventStorage {

private int maxSize;

private LinkedList<Date> storage;

2. Implemente o construtor da classe que inicializa os seus atributos. public EventStorage(){

maxSize=10;

storage=new LinkedList<>();

}

Programa Exemplo

3. Implemente o método synchronized set() para armazenar um evento. Primeiro, verifique se o buffer está cheio ou não. Se ele estiver cheio, chame o método wait() até que tenha espaço vazio. No final do método, chamamos o método notifyAll() para acordar todos as threads que estão dormindo no método wait(). public synchronized void set(){

while (storage.size()==maxSize){

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

storage.offer(new Date());

System.out.printf("Set: %d\n",storage.size());

notifyAll();

}

Programa Exemplo

4. Implementar o método synchronized get() para obter um evento do armazenamento. Primeiro, verifique se o buffer tem eventos ou não. Se ele não tem eventos, chame o método wait() até que tenha algum evento. No final do método, chamamos o método notifyAll() para acordar todas as threads que estão dormindo no método wait(). public synchronized void get(){

while (storage.size()==0){

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

System.out.printf("Get: %d: %s\n",storage.size(),

((LinkedList<?>)storage).poll());

notifyAll();

}

Programa Exemplo

5. Crie uma classe chamada Producer e especifique que ela implementa a interface Runnable. Ela irá implementar o produtor do exemplo. public class Producer implements Runnable {

6. Declare um objeto EventStorage e implemente o construtor da classe, que inicializa este objeto. private EventStorage storage;

public Producer(EventStorage storage){

this.storage=storage;

}

Programa Exemplo

7. Implemente o método run() que chama 100 vezes o método set() do objeto EventStorage. @Override

public void run() {

for (int i=0; i<100; i++){

storage.set();

}

}

8. Crie uma classe chamada Consumer e especifique que ela implementa a interface Runnable. Ela irá implementar o consumidor para o exemplo. public class Consumer implements Runnable {

Programa Exemplo

9. Declare um objeto EventStorage e implemente o construtor da classe que inicializa este objeto. private EventStorage storage;

public Consumer(EventStorage storage){

this.storage=storage;

}

10. Implemente o método run(). Ele chama 100 vezes o método get() do objeto EventStorage. @Override

public void run() {

for (int i=0; i<100; i++){

storage.get();

}

}

Programa Exemplo

11. Crie a classe principal para o exemplo e adicione o método main(). public class Main {

public static void main(String[] args) {

12. Crie um objeto EventStorage. EventStorage storage=new EventStorage();

13. Crie um objeto Producer e a Thread para executá-lo. Producer producer=new Producer(storage);

Thread thread1=new Thread(producer);

Programa Exemplo

14. Crie um objeto Consumer e a Thread para executá-lo. Consumer consumer=new Consumer(storage);

Thread thread2=new Thread(consumer);

15. Inicie ambas as threads. thread2.start();

thread1.start();

Funcionamento

• A chave para este exemplo são os métodos set() e get() da classe EventStorage

• Primeiro, o método set() verifica se há espaço livre no atributo de armazenamento

– Se ele está cheio, ele chama o método wait() para aguardar espaço livre

• Quando a outra thread chama o método notifyAll(), a thread acorda e verifica a condição novamente

– O método notifyAll() não garante que a thread vai acordar

• Este processo é repetido até que haja espaço livre no armazenamento, então ela pode gerar um novo evento e armazená-lo

Funcionamento

• O comportamento do método get() é semelhante, primeiro, ele verifica se há eventos no armazenamento – Se a classe EventStorage está vazia, ele chama o método wait() para aguardar eventos

– Quando a outra thread chama o método notifyAll(), a thread acorda e verifica a condição novamente até que existam alguns eventos no armazenamento

• Temos que manter a verificação das condições e chamar o método wait() em um laço while, não podemos continuar até que a condição seja verdadeira

• Se executarmos esse exemplo, vamos ver como o produtor e o consumidor estão criando e obtendo os eventos, mas o armazenamento nunca possui mais do de 10 eventos

Sincronizando um Bloco de Código com um Bloqueio

• Java fornece um outro mecanismo para a sincronização de blocos de código

• É um mecanismo mais poderoso e flexível do que a palavra-chave synchronized

• Ele é baseado na interface Lock e classes que a implementam (como ReentrantLock)

Sincronizando um Bloco de Código com um Bloqueio

• Este mecanismo apresenta algumas vantagens, que são as seguintes:

– Ele permite a estruturação de blocos sincronizados de uma maneira mais flexível. Com a palavra-chave synchronized, temos que obter e liberar o controle sobre um bloco sincronizado de código de uma forma estruturada. As interfaces Lock permitem que obtenhamos estruturas mais complexas para implementar a seção crítica.

Sincronizando um Bloco de Código com um Bloqueio

• Vantagens (continuação):

– As interfaces Lock fornecem funcionalidades adicionais sobre a palavra-chave synchronized. Uma das novas funcionalidades é implementada pelo método tryLock(). Este método tenta obter o controle do bloqueio e se não pode, porque ele está sendo usado por outra thread, ele retorna o bloqueio. Com a palavra-chave synchronized, quando uma thread (A) tenta executar um bloco de código sincronizado, se houver outra thread (B) executando-o, a thread (A) é suspensa até que a thread (B) termine a execução do bloco sincronizado. Com bloqueios, podemos executar o método tryLock(). Esse método retorna um valor booleano que indica se há outra thread executando o código protegido por esse bloqueio.

Sincronizando um Bloco de Código com um Bloqueio

• Vantagens (continuação):

– As interfaces Lock permitem uma separação das operações de leitura e gravação que possuem vários leitores e apenas um modificador.

– As interfaces Lock oferecer um melhor desempenho do que a palavra-chave synchronized.

• Neste exemplo, vamos aprender como usar bloqueios para sincronizar um bloco de código e criar uma seção crítica usando a interface Lock e a classe ReentrantLock

que a implementa, implementando um programa que simula uma fila de impressão

Programa Exemplo

1. Crie uma classe chamada PrintQueue que irá implementar a fila de impressão. public class PrintQueue {

2. Declare um objeto Lock e o inicialize com um novo objeto da classe ReentrantLock. private final Lock queueLock=new

ReentrantLock();

3. Implemente o método printJob(). Ele irá receber um Object como parâmetro e não irá retornar nenhum valor. public void printJob(Object document){

Programa Exemplo

4. Dentro do método printJob() obtenha o controle do objeto Lock chamando o método lock(). lock() method.

queueLock.lock();

5. Então, inclua o código a seguir para simular a impressão de um documento: try {

Long duration=(long)(Math.random()*10000);

System.out.println(Thread.currentThread()

.getName()+ ":Fila de Impressão:

Imprimindo um trabalho durante "+

(duration/1000)+" segundos");

Thread.sleep(duration);

} catch (InterruptedException e) {

e.printStackTrace();

}

Programa Exemplo

6. Finalmente, libere o controle do objeto Lock com o método unlock(). finally {

queueLock.unlock();

}

7. Crie uma classe chamada Job e especifique que ela implementa a interface Runnable. public class Job implements Runnable {

8. Declare um objeto da classe PrintQueue e implemente o construtor da classe que inicializa esse objeto. private PrintQueue printQueue;

public Job(PrintQueue printQueue){

this.printQueue=printQueue;

}

Programa Exemplo

9. Implemente o método run(). Ele usa o objeto PrintQueue para enviar um trabalho para a impressora. @Override

public void run() {

System.out.printf("%s: Imprimindo um

documento\n", Thread.currentThread()

.getName());

printQueue.printJob(new Object());

System.out.printf("%s: O documento foi

impresso\n",Thread.currentThread()

.getName());

}

10. Crie a classe principal da aplicação e adicione o método main() a ela. public class Main {

public static void main (String args[]){

Programa Exemplo

11. Crie um objeto compartilhado PrintQueue. PrintQueue printQueue=new PrintQueue();

12. Cria 10 objetos Job e 10 threads pra executá-los. Thread thread[]=new Thread[10];

for (int i=0; i<10; i++){

thread[i]=new Thread(new Job(printQueue),

"Thread "+ i);

}

13. Inicie as 10 threads. for (int i=0; i<10; i++){

thread[i].start();

}

Funcionamento

• Na captura de tela seguinte, podemos ver uma parte da saída de uma execução, deste exemplo:

Funcionamento

• A chave para o exemplo é o método printJob() da classe PrintQueue

• Quando queremos implementar uma seção crítica usando bloqueios e garantir que apenas uma thread de execução execute um bloco de código, temos que criar um objeto ReentrantLock

• No início da seção crítica, temos de começar o controle do bloqueio utilizando o método lock()

• Quando uma thread (A) chama este método, se não houver outra thread com o controle do bloqueio, o método permite que a thread (A) controle o bloqueio e retorna imediatamente para permitir a execução da seção crítica por esta thread

• Caso contrário, se há uma outra thread (B) executando a seção crítica controlada por este bloqueio, o método lock() coloca a thread (A) para dormir até que a thread (B) termine a execução da seção crítica

Funcionamento

• No final da seção crítica, temos de usar o método unlock() para liberar o controle do bloqueio e permitir que as outras threads executem esta seção crítica

• Se não chamarmos o método unlock() no final da seção crítica, as outras threads que estão aguardando por esse bloco ficarão esperando para sempre, causando uma situação de impasse (deadlock)

• Se usarmos blocos try-catch em uma seção crítica, não podemos esquecer de colocar o comando que contém o método unlock() dentro da seção finally

Sincronizando um Bloco de Código com um Bloqueio

• A interface Lock (e a classe ReentrantLock) inclui um outro método para obter o controle do bloqueio, é o método tryLock()

• A maior diferença com o método lock() é que neste método, se a thread que o usa não pode obter o controle da interface Lock, retorna imediatamente e não coloca a thread para dormir

• Este método retorna um valor booleano, true se a thread recebe o controle do bloqueio, e false se não

• Leve em consideração que é responsabilidade do programador ter em conta o resultado deste método e agir em conformidade

Sincronizando um Bloco de Código com um Bloqueio

• Se o método retorna o valor false, espera-se que o programa não execute a seção crítica

• Se isso acontecer, provavelmente teremos resultados errados na aplicação

Sincronizando um Bloco de Código com um Bloqueio

• A classe ReentrantLock também permite o uso de chamadas recursivas

• Quando uma thread tem o controle de um bloqueio e faz uma chamada recursiva, continua com o controle do bloqueio, de modo que a chamada ao método lock() retornará imediatamente e a thread vai continuar com a execução da chamada recursiva

• Além disso, também pode chamar outros métodos

Sincronizando um Bloco de Código com um Bloqueio

• Devemos ter muito cuidado com o uso de bloqueios para evitar impasses (deadlocks)

• Esta situação ocorre quando duas ou mais threads são bloqueadas à espera de bloqueios que nunca vão ser desbloqueados

• Por exemplo, uma thread (A) bloqueia um Lock (X) e uma thread (B) bloqueia um Lock (Y)

• Se agora, a thread (A) tenta bloquear um Lock (Y) e a thread (B) simultaneamente tenta bloquear um Lock (X), ambas as threads serão bloqueadas indefinidamente, porque elas estão à espera de bloqueios que nunca vão ser liberados

• Note-se que o problema ocorre, porque ambas as threads tentam obter os bloqueios na ordem oposta

Sincronizando Acesso a Dados com Bloqueios de Leitura/Escrita

• Uma das melhorias mais significativas oferecidos por bloqueios é a interface ReadWriteLock e a classe ReentrantReadWriteLock, a única que a implementa

• Essa classe tem dois bloqueios, um para operações de leitura e um para operações de gravação

• Pode haver mais de uma thread usando operações de leitura simultaneamente, mas somente uma thread pode estar usando operações de gravação

• Quando uma thread está fazendo uma operação de gravação, não pode haver qualquer outra thread fazendo operações de leitura

• Neste exemplo, vamos aprender como usar uma interface ReadWriteLock implementando um programa que a utiliza para controlar o acesso a um objeto que armazena os preços de dois produtos

Programa Exemplo

1. Crie uma classe chamada PricesInfo que armazenará informação sobre os preços de dois produtos. public class PricesInfo {

2. Declare dois atributos double chamados price1 e price2. private double price1;

private double price2;

3. Declare um objeto ReadWriteLock chamado lock. private ReadWriteLock lock;

Programa Exemplo

4. Implemente o construtor da classe que inicializa estes três atributos. Para o atributo lock, criaremos um novo objeto ReentrantReadWriteLock.

public PricesInfo(){

price1=1.0;

price2=2.0;

lock=new ReentrantReadWriteLock();

}

Programa Exemplo

5. Implemente o método getPrice1() que retorna o valor do atributo price1. Ele utiliza o bloqueio de leitura para controlar o acesso ao valor deste atributo.

public double getPrice1() {

lock.readLock().lock();

double value=price1;

lock.readLock().unlock();

return value;

}

Programa Exemplo

6. Implemente o método getPrice2() que retorna o valor do atributo price2. Ele utiliza o bloqueio de leitura para controlar o acesso ao valor deste atributo.

public double getPrice2() {

lock.readLock().lock();

double value=price2;

lock.readLock().unlock();

return value;

}

Programa Exemplo

7. Implemente o método setPrices() que estabelece os valores dos dois atributos. Ele utiliza o bloqueio de escrita para controlar o acesso a eles.

public void setPrices(double price1,

double price2) {

lock.writeLock().lock();

this.price1=price1;

this.price2=price2;

lock.writeLock().unlock();

}

Programa Exemplo

8. Crie uma classe chamada Reader e especifique que ela implementa a interface Runnable. Esta classe implementa um leitor de valores dos atributos da classe PricesInfo. public class Reader implements Runnable {

9. Declare um objeto PricesInfo e implemente o construtor da classe que inicializa esse objeto. private PricesInfo pricesInfo;

public Reader (PricesInfo pricesInfo){

this.pricesInfo=pricesInfo;

}

Programa Exemplo

10. Implemente o método run() desta classe. Ele lê 10 vezes o valor dos dois preços. @Override

public void run() {

for (int i=0; i<10; i++){

System.out.printf("%s: Price 1: %f\n",

Thread.currentThread().getName(),

pricesInfo.getPrice1());

System.out.printf("%s: Price 2: %f\n",

Thread.currentThread().getName(),

pricesInfo.getPrice2());

}

}

Programa Exemplo

11. Crie uma classe chamada Writer e especifique que ela implementa a interface Runnable. Esta classe implementa um modificador dos valores dos atributos da classe PricesInfo. public class Writer implements Runnable {

12. Declare um objeto PricesInfo e implemente o construtor da classe que inicializa esse objeto. private PricesInfo pricesInfo;

public Writer(PricesInfo pricesInfo){

this.pricesInfo=pricesInfo;

}

Programa Exemplo

13. Implemente o método run(). Ele modifica três vezes o valor dos dois preços e dorme por dois segundos entre as alterações. @Override

public void run() {

for (int i=0; i<3; i++) {

System.out.printf("Writer: Tentando

modificar os preços.\n");

pricesInfo.setPrices(Math.random()*10,

Math.random()*8);

System.out.printf("Writer: Os preços foram

modificados.\n");

try {

Thread.sleep(2);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

Programa Exemplo

14. Implemente a classe principal do exemplo e crie o método main(). public class Main {

public static void main(String[] args) {

15. Crie um objeto PricesInfo. PricesInfo pricesInfo=new PricesInfo();

16. Crie cinco objetos Reader e cinco Threads para executá-los. Reader readers[]=new Reader[5];

Thread threadsReader[]=new Thread[5];

for (int i=0; i<5; i++){

readers[i]=new Reader(pricesInfo);

threadsReader[i]=new Thread(readers[i]);

}

Programa Exemplo

17. Crie um objeto Writer e uma Thread para executá-lo. Writer writer=new Writer(pricesInfo);

Thread threadWriter=new Thread(writer);

18. Inicie as threads. for (int i=0; i<5; i++){

threadsReader[i].start();

}

threadWriter.start();

Funcionamento

• Na captura de tela seguinte, você pode ver uma parte da saída de uma execução deste exemplo:

Funcionamento

• Como mencionamos anteriormente, a classe ReentrantReadWriteLock tem dois bloqueios, um para operações de leitura e um para operações de gravação

• O bloqueio utilizado em operações de leitura é obtida com o método readlock() declarado na interface ReadWriteLock

• Esse bloqueio é um objeto que implementa a interface Lock, para que possamos usar os métodos lock(), unlock(), e tryLock()

• O bloqueio utilizado em operações de gravação é obtido com o método writeLock() declarado na interface ReadWriteLock

Funcionamento

• Esse bloqueio é um objeto que implementa a interface Lock, para que possamos usar os métodos lock(), unlock(), e tryLock()

• É de responsabilidade do programador garantir o uso correto desses bloqueios, usando-os com os mesmos propósitos para os quais eles foram projetados

• Quando conseguimos o bloqueio de leitura de uma interface Lock, não podemos modificar o valor da variável, caso contrário, provavelmente teríamos erros de inconsistência de dados

Modificando o Equilíbrio entre Bloqueios

• O construtor das classes ReentrantLock e ReentrantReadWriteLock admite um parâmetro boolean chamado fair que permite controlar o comportamento de ambas as classes

• O valor false é o valor padrão e é chamado de modo não justo – Neste modo, quando existem algumas threads esperando por um

bloqueio (ReentrantLock ou ReentrantReadWriteLock) e o bloqueio tem que selecionar uma delas para obter o acesso à seção crítica, ele seleciona uma sem qualquer critério

• O valor true é chamado de modo justo – Neste modo, quando existem algumas threads esperando por um

bloqueio (ReentrantLock ou ReentrantReadWriteLock) e o bloqueio tem que selecionar uma delas para obter acesso a uma seção crítica, ele seleciona a thread que está esperando por mais tempo

Modificando o Equilíbrio entre Bloqueios

• Leve em consideração que o comportamento explicado anteriormente só é usado com os métodos lock() e unlock()

• Como o método tryLock() não coloca a thread para dormir, se a interface Lock é usada, o atributo fair não afeta a sua funcionalidade

• Neste exemplo, vamos modificar o exemplo implementado em Sincronizando um Bloco de Código com um Bloqueio para usar esse atributo e ver a diferença entre os modos justo e não justo

Programa Exemplo

1. Implemente o exemplo explicado em Sincronizando um Bloco de Código com um Bloqueio.

2. Na classe PrintQueue, modifique a construção do objeto Lock. private Lock queueLock=new

ReentrantLock(true);

3. Modifique o método printJob(). Separe o simulador de impressão em dois blocos de código, liberando o bloqueio entre eles.

Programa Exemplo

public void printJob(Object document){

queueLock.lock();

try {

Long duration=(long)(Math.random()*10000);

System.out.println(Thread.currentThread().

getName()+":Fila de Impressão:

Imprimindo um trabalho durante "+

(duration/1000)+" segundos");

Thread.sleep(duration);

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

queueLock.unlock();

}

...

Programa Exemplo

...

queueLock.lock();

try {

Long duration=(long)(Math.random()*10000);

System.out.println(Thread.currentThread().

getName()+":Fila de Impressão:

Imprimindo um trabalho durante "+

(duration/1000)+" segundos");

Thread.sleep(duration);

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

queueLock.unlock();

}

}

Programa Exemplo

4. Modifique a classe principal no bloco que inicia as threads. O novo bloco deve ser: for (int i=0; i<10; i++){

thread[i].start();

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

Funcionamento

• Na captura de tela seguinte você pode ver uma parte da saída de uma execução deste exemplo:

Funcionamento

• Todas as threads são criadas com uma diferença de 0,1 segundo

• A primeira thread que solicita o controle do bloqueio é a Thread 0, então a Thread 1, e assim por diante

• Enquanto Thread 0 está executando o primeiro bloco de código protegido pelo bloqueio, temos nove threads em espera para executar esse bloco de código

• Quando a Thread 0 libera o bloqueio, imediatamente, ela solicita o bloqueio novamente, por isso temos 10 threads que tentam obter o bloqueio

• Como o modo justo está habilitado, a interface Lock vai escolher Thread 1, uma vez que é a thread que está esperando por mais tempo pelo bloqueio

Funcionamento

• Em seguida, escolhe Thread 2, então, o Thread 3, e assim por diante

• Até que todas as threads tenham passado pelo primeiro bloco protegido pelo bloqueio, nenhuma delas vai executar o segundo bloco protegido

• Uma vez que todas as threads tenham executado o primeiro bloco de código protegido pelo bloqueio, é a vez de Thread 0 novamente, em seguida, é a vez de Thread 1, e assim por diante

• Para ver a diferença com o modo não justo, altere o parâmetro passado para o construtor de bloqueio e coloque o valor false

Funcionamento

• Na captura de tela seguinte, você pode ver o resultado de uma execução do exemplo modificado:

Funcionamento

• Neste caso, as threads são executadas pela ordem em que foram criadas, mas cada thread executa os dois blocos de código protegidos

• No entanto, esse comportamento não é garantido porque, como explicado anteriormente, o bloqueio poderia escolher qualquer thread para dar-lhe acesso ao código protegido

• A JVM não garante, neste caso, a ordem de execução das threads

• Bloqueios Read/Write também têm o parâmetro fair em seu construtor, o comportamento deste parâmetro neste tipo de bloqueio é o mesmo que foi explicado na introdução deste exemplo

Usando Múltiplas Condições em um Bloqueio

• Um bloqueio pode ser associado a uma ou mais condições

• Estas condições são declaradas na interface Condition

• O objetivo destas condições é permitir que threads tenham o controle de um bloqueio e verifiquem se uma condição é verdadeira ou não e, se for falsa, ser suspensa até que uma outra thread a acorde

• A interface Condition fornece os mecanismos para suspender uma thread e para acordar uma thread suspensa

Usando Múltiplas Condições em um Bloqueio

• Um problema clássico na programação concorrente é o problema do produtor-consumidor

• Temos um buffer de dados, um ou mais produtores de dados que armazenam no buffer, e um ou mais consumidores de dados que leem a partir do buffer como explicado anteriormente

• Neste exemplo, veremos como implementar o problema do produtor-consumidor usando bloqueios e condições

Programa Exemplo

1. Primeiro, vamos implementar uma classe que irá simular um arquivo de texto. Crie uma classe chamada FileMock com dois atributos: um array String

denominado content e um int chamado index. Eles vão armazenar o conteúdo do arquivo e a linha do arquivo simulado que será recuperada. public class FileMock {

private String content[];

private int index;

Programa Exemplo

2. Implemente o construtor da classe que inicializa o conteúdo e o arquivo com caracteres aleatórios. public FileMock(int size, int length){

content=new String[size];

for (int i=0; i<size; i++){

StringBuilder buffer =

new StringBuilder(length);

for (int j=0; j<length; j++){

int indice=(int)Math.random()*255;

buffer.append((char)indice);

}

content[i]=buffer.toString();

}

index=0;

}

Programa Exemplo

3. Implemente o método hasMoreLines() que retorna true se o arquivo tem mais linhas para processar ou false se já atingiu o fim do arquivo simulado. public boolean hasMoreLines(){

return index < content.length;

}

4. Implemente o método getLine() que retorna a linha determinada pelo atributo index e aumenta o seu valor. public String getLine(){

if (this.hasMoreLines()) {

System.out.println("Mock: "+

(content.length-index));

return content[index++];

}

return null;

}

Programa Exemplo

5. Agora implemente uma classe chamada Buffer que irá implementar o buffer compartilhado por produtores e consumidores. public class Buffer {

6. Esta classe possui seis atributos:

– Um atributo LinkedList<String> chamado buffer que irá armazenar os dados compartilhados

– Um int chamado maxSize que armazena o tamanho do buffer

– Um objeto ReentrantLock chamado lock que controla o acesso aos blocos de código que modificam o buffer

– Dois atributos Condition chamados lines e space

– Um tipo boolean chamado pendingLines que irá indicar se existem linhas no buffer

Programa Exemplo

private LinkedList<String> buffer;

private int maxSize;

private ReentrantLock lock;

private Condition lines;

private Condition space;

private boolean pendingLines;

Programa Exemplo

7. Implemente o construtor da classe. Ele inicializa todos os atributos descritos anteriormente. public Buffer(int maxSize) {

this.maxSize=maxSize;

buffer=new LinkedList<>();

lock=new ReentrantLock();

lines=lock.newCondition();

space=lock.newCondition();

pendingLines=true;

}

Programa Exemplo

8. Implemente o método insert(). Ele recebe uma String como parâmetro e tenta armazená-lo no buffer. Em primeiro lugar, ele obtém o controle do bloqueio. Quando consegue, então verifica se existe espaço vazio no buffer. Se o buffer está cheio, ele chama o método await() na condição space para esperar por espaço livre. A thread será acordada quando outra thread chamar o método de signal() ou signalAll() na Condition space. Quando isso acontece, a thread armazena a linha no buffer e chama o método signallAll() sobre a condição lines. Como veremos em um momento, essa condição vai acordar todas as threads que estão à espera de linhas no buffer.

Programa Exemplo

public void insert(String line) {

lock.lock();

try {

while (buffer.size() == maxSize) {

space.await();

}

buffer.offer(line);

System.out.printf("%s: Linha inserida: %d\n",

Thread.currentThread().getName(),

buffer.size());

lines.signalAll();

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.unlock();

}

}

Programa Exemplo

9. Implemente o método get(). Ele retorna a primeira string armazenada no buffer. Em primeiro lugar, ele obtém o controle do bloqueio. Quando consegue, ele verifica se há linhas no buffer. Se o buffer está vazio, ele chama o método await() na condição lines para esperar por linhas no buffer. Esta thread será acordada quando uma outra thread chamar o método signal() ou signalAll() na condição lines. Quando isso acontece, o método obtém a primeira linha no buffer, chama o método signalAll() sobre a condição space

e retorna a String.

Programa Exemplo

public String get() {

String line=null;

lock.lock();

try {

while ((buffer.size() == 0) &&(hasPendingLines())) {

lines.await();

}

if (hasPendingLines()) {

line = buffer.poll();

System.out.printf("%s: Linha carregada: %d\n",

Thread.currentThread().getName(),buffer.size());

space.signalAll();

}

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.unlock();

}

return line;

}

Programa Exemplo

10. Implemente o método setPendingLines() que estabelece o valor do atributo pendingLines. Ele será chamado pelo producer quando não existirem mais linhas para produzir. public void setPendingLines(boolean

pendingLines) {

this.pendingLines=pendingLines; }

11. Implemente o método hasPendingLines. Ele retorna true se existem mais linhas a serem processadas, ou false caso contrário. public boolean hasPendingLines() {

return pendingLines || buffer.size()>0;

}

Programa Exemplo

12. Implemente uma classe chamada Producer e especifique que ela implementa a interface Runnable. public class Producer implements Runnable {

13. Declare dois atributos: um objeto da classe FileMock e outro objeto da classe Buffer. private FileMock mock;

private Buffer buffer;

14. Implemente o construtor da classe que inicializa ambos os atributos. public Producer (FileMock mock, Buffer buffer){

this.mock=mock;

this.buffer=buffer;

}

Programa Exemplo

15. Implemente o método run() que lê todas as linhas criadas no objeto FileMock e usa o método insert() para armazená-las no buffer. Uma vez terminado, use o método setPendingLines() para alertar o buffer que ele não precisa gerar mais linhas. @Override

public void run() {

buffer.setPendingLines(true);

while (mock.hasMoreLines()){

String line=mock.getLine();

buffer.insert(line);

}

buffer.setPendingLines(false);

}

Programa Exemplo

16. Implemente uma classe chamada Consumer e especifique que ela implementa a interface Runnable. public class Consumer implements Runnable {

17. Declare um objeto Buffer e implemente o construtor da classe que o inicializa. private Buffer buffer;

public Consumer (Buffer buffer) {

this.buffer=buffer;

}

Programa Exemplo

18. Implemente o método run(). Enquanto o buffer possuir linhas pendentes, ele tenta recuperar uma e processá-la. @Override

public void run() {

while (buffer.hasPendingLines()) {

String line=buffer.get();

processLine(line);

}

}

Programa Exemplo

19. Implemente o método auxiliar processLine(). Ele apenas espera por até 10 milissegundos para simular algum tipo de processamento com a linha. private void processLine(String line) {

try {

Random random=new Random();

Thread.sleep(random.nextInt(100));

} catch (InterruptedException e) {

e.printStackTrace();

}

}

Programa Exemplo

20. Implemente a classe principal do exemplo e adicione o método main(). public class Main {

public static void main(String[] args) {

21. Crie um objeto FileMock. FileMock mock=new FileMock(100, 10);

22. Crie um objeto Buffer. Buffer buffer=new Buffer(20);

23. Crie um objeto Producer e uma Thread para executá-lo. Producer producer=new Producer(mock,

buffer);

Thread threadProducer=new

Thread(producer,"Producer");

Programa Exemplo

24. Crie três objetos Consumer e três threads para executá-los. Consumer consumers[]=new Consumer[3];

Thread threadConsumers[]=new Thread[3];

for (int i=0; i<3; i++){

consumers[i]=new Consumer(buffer);

threadConsumers[i]=new

Thread(consumers[i],"Consumer "+i);

}

Programa Exemplo

25. Inicie o produtor e os três consumidores. threadProducer.start();

for (int i=0; i<3; i++){

threadConsumers[i].start();

}

Funcionamento

• Todos os objetos Condition estão associados com um bloqueio e são criados usando o método newCondition() declarado na interface Lock

• Antes de podermos fazer qualquer operação com uma condição, temos que ter o controle do bloqueio associado com a condição, por isso as operações com condições devem estar em um bloco de código que começa com uma chamada a um método lock() de um objeto Lock e termina com um método unlock() do mesmo objeto Lock

• Quando uma thread chama o método await() de uma condição, ela libera automaticamente o controle do bloqueio, de modo que alguma outra thread pode obtê-lo e começar a execução da mesma, ou de outra seção crítica protegida por esse bloqueio

Funcionamento

• Quando uma thread chama os métodos signal() ou signallAll() de uma condição, uma ou todas as threads que estavam esperando por essa condição serão acordadas, mas isso não garante que a condição que fez com que elas dormissem seja agora verdadeira, por isso devemos colocar a chamada await() dentro de um laço while

• Não podemos deixar esse ciclo até que a condição seja verdadeira

• Enquanto a condição for falsa, devemos chamar await() novamente

Funcionamento

• Devemos ter cuidado com o uso de await() e signal(): se chamarmos o método await() em uma condição e nunca chamarmos o método de signal() nesta condição, a thread irá dormir para sempre

• Uma thread pode ser interrompida enquanto ele está dormindo, depois de uma chamada ao método await(), então temos que processar a exceção InterruptedException

Usando Múltiplas Condições em um Bloqueio

• A interface Condition tem outras versões do método await(), que são os seguintes:

– await(long time, TimeUnit unit): a thread irá dormir até que: • Seja interrompida

• Uma outra thread chama os métodos signal() ou signalAll() na condição

• O tempo especificado passe

• A classe TimeUnit é uma enumeração com as seguintes constantes: DAYS, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS e SECONDS

– awaitUninterruptibly(): a thread irá dormir até que uma outra thread chame os métodos signal() ou signalAll(), que não podem ser interrompidos

Usando Múltiplas Condições em um Bloqueio

• A interface Condition tem outras versões do método await(), que são os seguintes (continuação):

– awaitUntil(Date date): a thread ficará dormindo até que: • Seja interrompida

• Uma outra thread chama os métodos signal() ou signalAll() na condição

• A data especificada chegue

• Podemos usar condições com os bloqueios ReadLock e WriteLock de leitura/escrita

UNIVERSIDADE ESTADUAL DO SUDOESTE DA BAHIA CURSO DE CIÊNCIA DA COMPUTAÇÃO

PROGRAMAÇÃO CONCORRENTE – 2015.1

Fábio M. Pereira

(fabio.mpereira@uesb.edu.br)

top related