05 - Sincronização de Threads - I

137
UNIVERSIDADE ESTADUAL DO SUDOESTE DA BAHIA CURSO DE CIÊNCIA DA COMPUTAÇÃO PROGRAMAÇÃO CONCORRENTE – 2015.1 Fábio M. Pereira ([email protected])

Transcript of 05 - Sincronização de Threads - I

Page 1: 05 - Sincronização de Threads - I

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

PROGRAMAÇÃO CONCORRENTE – 2015.1

Fábio M. Pereira

([email protected])

Page 2: 05 - Sincronização de Threads - I

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

Page 3: 05 - Sincronização de Threads - I
Page 4: 05 - Sincronização de Threads - I

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.

Page 5: 05 - Sincronização de Threads - I

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

Page 6: 05 - Sincronização de Threads - I

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

Page 7: 05 - Sincronização de Threads - I
Page 8: 05 - Sincronização de Threads - I

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

Page 9: 05 - Sincronização de Threads - I

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

Page 10: 05 - Sincronização de Threads - I

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

Page 11: 05 - Sincronização de Threads - I

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;

}

Page 12: 05 - Sincronização de Threads - I

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;

}

Page 13: 05 - Sincronização de Threads - I

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;

}

Page 14: 05 - Sincronização de Threads - I

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;

}

Page 15: 05 - Sincronização de Threads - I

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 {

Page 16: 05 - Sincronização de Threads - I

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);

}

}

Page 17: 05 - Sincronização de Threads - I

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);

Page 18: 05 - Sincronização de Threads - I

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();

Page 19: 05 - Sincronização de Threads - I

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();

}

Page 20: 05 - Sincronização de Threads - I

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

Page 21: 05 - Sincronização de Threads - I

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

Page 22: 05 - Sincronização de Threads - I

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

Page 23: 05 - Sincronização de Threads - I

Funcionamento

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

Page 24: 05 - Sincronização de Threads - I

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

Page 25: 05 - Sincronização de Threads - I

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:

Page 26: 05 - Sincronização de Threads - I

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

Page 27: 05 - Sincronização de Threads - I

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

Page 28: 05 - Sincronização de Threads - I

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

Page 29: 05 - Sincronização de Threads - I

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

}

Page 30: 05 - Sincronização de Threads - I

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

Page 31: 05 - Sincronização de Threads - I
Page 32: 05 - Sincronização de Threads - I

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

Page 33: 05 - Sincronização de Threads - I

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

Page 34: 05 - Sincronização de Threads - I

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;

Page 35: 05 - Sincronização de Threads - I

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;

}

Page 36: 05 - Sincronização de Threads - I

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;

}

}

}

Page 37: 05 - Sincronização de Threads - I

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;

}

}

}

Page 38: 05 - Sincronização de Threads - I

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;

}

}

Page 39: 05 - Sincronização de Threads - I

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;

}

}

Page 40: 05 - Sincronização de Threads - I

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 {

Page 41: 05 - Sincronização de Threads - I

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;

}

Page 42: 05 - Sincronização de Threads - I

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);

}

Page 43: 05 - Sincronização de Threads - I

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;

}

Page 44: 05 - Sincronização de Threads - I

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);

}

Page 45: 05 - Sincronização de Threads - I

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");

Page 46: 05 - Sincronização de Threads - I

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();

Page 47: 05 - Sincronização de Threads - I

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());

Page 48: 05 - Sincronização de Threads - I

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)

Page 49: 05 - Sincronização de Threads - I

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

Page 50: 05 - Sincronização de Threads - I

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:

Page 51: 05 - Sincronização de Threads - I
Page 52: 05 - Sincronização de Threads - I

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

Page 53: 05 - Sincronização de Threads - I

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

Page 54: 05 - Sincronização de Threads - I

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()

Page 55: 05 - Sincronização de Threads - I

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<>();

}

Page 56: 05 - Sincronização de Threads - I

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();

}

Page 57: 05 - Sincronização de Threads - I

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();

}

Page 58: 05 - Sincronização de Threads - I

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;

}

Page 59: 05 - Sincronização de Threads - I

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 {

Page 60: 05 - Sincronização de Threads - I

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();

}

}

Page 61: 05 - Sincronização de Threads - I

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);

Page 62: 05 - Sincronização de Threads - I

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();

Page 63: 05 - Sincronização de Threads - I

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

Page 64: 05 - Sincronização de Threads - I

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

Page 65: 05 - Sincronização de Threads - I
Page 66: 05 - Sincronização de Threads - I

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)

Page 67: 05 - Sincronização de Threads - I

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.

Page 68: 05 - Sincronização de Threads - I

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.

Page 69: 05 - Sincronização de Threads - I

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

Page 70: 05 - Sincronização de Threads - I

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){

Page 71: 05 - Sincronização de Threads - I

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();

}

Page 72: 05 - Sincronização de Threads - I

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;

}

Page 73: 05 - Sincronização de Threads - I

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[]){

Page 74: 05 - Sincronização de Threads - I

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();

}

Page 75: 05 - Sincronização de Threads - I

Funcionamento

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

Page 76: 05 - Sincronização de Threads - I

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

Page 77: 05 - Sincronização de Threads - I

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

Page 78: 05 - Sincronização de Threads - I

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

Page 79: 05 - Sincronização de Threads - I

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

Page 80: 05 - Sincronização de Threads - I

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

Page 81: 05 - Sincronização de Threads - I

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

Page 82: 05 - Sincronização de Threads - I
Page 83: 05 - Sincronização de Threads - I

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

Page 84: 05 - Sincronização de Threads - I

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;

Page 85: 05 - Sincronização de Threads - I

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();

}

Page 86: 05 - Sincronização de Threads - I

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;

}

Page 87: 05 - Sincronização de Threads - I

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;

}

Page 88: 05 - Sincronização de Threads - I

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();

}

Page 89: 05 - Sincronização de Threads - I

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;

}

Page 90: 05 - Sincronização de Threads - I

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());

}

}

Page 91: 05 - Sincronização de Threads - I

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;

}

Page 92: 05 - Sincronização de Threads - I

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();

}

}

}

Page 93: 05 - Sincronização de Threads - I

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]);

}

Page 94: 05 - Sincronização de Threads - 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();

Page 95: 05 - Sincronização de Threads - I

Funcionamento

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

Page 96: 05 - Sincronização de Threads - I

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

Page 97: 05 - Sincronização de Threads - I

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

Page 98: 05 - Sincronização de Threads - I
Page 99: 05 - Sincronização de Threads - I

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

Page 100: 05 - Sincronização de Threads - I

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

Page 101: 05 - Sincronização de Threads - I

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.

Page 102: 05 - Sincronização de Threads - I

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();

}

...

Page 103: 05 - Sincronização de Threads - I

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();

}

}

Page 104: 05 - Sincronização de Threads - I

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();

}

}

Page 105: 05 - Sincronização de Threads - I

Funcionamento

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

Page 106: 05 - Sincronização de Threads - I

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

Page 107: 05 - Sincronização de Threads - I

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

Page 108: 05 - Sincronização de Threads - I

Funcionamento

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

Page 109: 05 - Sincronização de Threads - I

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

Page 110: 05 - Sincronização de Threads - I
Page 111: 05 - Sincronização de Threads - I

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

Page 112: 05 - Sincronização de Threads - I

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

Page 113: 05 - Sincronização de Threads - I

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;

Page 114: 05 - Sincronização de Threads - I

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;

}

Page 115: 05 - Sincronização de Threads - I

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;

}

Page 116: 05 - Sincronização de Threads - I

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

Page 117: 05 - Sincronização de Threads - I

Programa Exemplo

private LinkedList<String> buffer;

private int maxSize;

private ReentrantLock lock;

private Condition lines;

private Condition space;

private boolean pendingLines;

Page 118: 05 - Sincronização de Threads - I

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;

}

Page 119: 05 - Sincronização de Threads - I

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.

Page 120: 05 - Sincronização de Threads - I

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();

}

}

Page 121: 05 - Sincronização de Threads - I

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.

Page 122: 05 - Sincronização de Threads - I

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;

}

Page 123: 05 - Sincronização de Threads - I

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;

}

Page 124: 05 - Sincronização de Threads - I

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;

}

Page 125: 05 - Sincronização de Threads - I

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);

}

Page 126: 05 - Sincronização de Threads - I

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;

}

Page 127: 05 - Sincronização de Threads - I

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);

}

}

Page 128: 05 - Sincronização de Threads - I

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();

}

}

Page 129: 05 - Sincronização de Threads - I

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");

Page 130: 05 - Sincronização de Threads - I

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);

}

Page 131: 05 - Sincronização de Threads - I

Programa Exemplo

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

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

threadConsumers[i].start();

}

Page 132: 05 - Sincronização de Threads - I

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

Page 133: 05 - Sincronização de Threads - I

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

Page 134: 05 - Sincronização de Threads - I

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

Page 135: 05 - Sincronização de Threads - I

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

Page 136: 05 - Sincronização de Threads - I

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

Page 137: 05 - Sincronização de Threads - I

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

PROGRAMAÇÃO CONCORRENTE – 2015.1

Fábio M. Pereira

([email protected])