Praticando concorrência em Java!

Neste post vamos mostrar um exemplo prático de programação com concorrência em Java explorando as ferramentas da linguagem.

E no começo haviam os monitores

Para começar vamos falar sobre uma estrutura básica na programação concorrente: o monitor.

Segundo [1] monitor é uma técnica utilizada para sincronizar tarefas que compartilham um recurso. ou seja, o monitor oferece uma interface para permitir a manipulação de um recurso compartilhado. Para tanto utiliza uma trava de exclusão mútua, que consiste de variáveis de controle e regras para liberar ou travar o recurso.

De maneira prática, o monitor basicamente é uma classe que controla o recurso, assim quando for preciso requisitá-lo devemos enviar o pedido para ela.

Produtor/Consumidor

Para exemplificar vamos analisar o problema do Produtor e Consumidor. Segundo [2]:

Uma ou mais thread de produtores criam um produto e colocam em um buffer. Uma ou mais thread de consumidores consomem o produto colocado no buffer. O produtor precisa esperar o buffer ficar livre para produzir o produto e o cliente precisa esperar o buffer ficar preenchido para consumir o produto.

A tarefa do problema é sincronizar o acesso ao recurso, no caso a pilha, para que produtores saibam quando podem produzir e consumidores saibam quando podem consumir. Vamos então para a parte prática!

Primeiro vamos criar a classe do Consumidor:

public class Consumidor extends Thread {
	private int idConsumidor;
	private Buffer pilha;
	private int totalConsumir;

	public Consumidor(int id, Buffer p, int totalConsumir) {
		idConsumidor = id;
		pilha = p;
		this.totalConsumir = totalConsumir;
	}

	public void run() {
		for (int i = 0; i < totalConsumir; i++) {
			pilha.get(idConsumidor);
		}
		System.out.println("Consumidor #" + idConsumidor + " concluido!");
	}
}

Essa classe é derivada da clase Thread, ou seja, cada cliente vai funcionar em um thread diferente. Os dados que utilizamos são um identificador (idConsumidor), uma referência para um Buffer e um contador (totalConsumir) que vai indicar quanto deve ser consumido pelo consumidor.

O método run() é chamado quando a thread for iniciada, ou seja, é nele que devemos definir o nosso cliente de fato. Definimos então o laço para executar as chamadas que consomem o recurso do buffer.

Vamos então definir a classe Produtor:

public class Produtor extends Thread {
	private int idProdutor;
	private Buffer pilha;
	private int producaoTotal;

	public Produtor(int id, Buffer p, int producaoTotal) {
		idProdutor = id;
		pilha = p;
		this.producaoTotal = producaoTotal;
	}

	public void run() {
		for (int i = 0; i < producaoTotal; i++) {
			pilha.set(idProdutor, i);
		}
		System.out.println("Produtor #" + idProdutor + " concluido!");
	}
}

De maneira semelhante ao Consumidor, o produtor possui um identificador, uma referência para um buffer e um total de produtos a serem produzidos. Note que essa classe também é uma Thread, ou seja, cada produtor será executado independentemente. O método run() também é bem semelhante ao do Consumidor, um laço que faz as chamadas ao buffer.

Sincronizando o acesso ao recurso compartilhado

Agora vem a parte principal, o buffer. Ele possui um conteúdo, que é colocado pelo produtor, e um booleano que indica quando o conteúdo está disponível.

Para permitir que um produtor coloque um produto ela oferece um método set, que carrega o conteúdo e avisa para as outras thread que o produto está disponível. Para dar acesso ao Consumidor, o buffer oferece um método get, que devolve o conteúdo e avisa para outras thread que o produto não está mais disponível.

Esta classe merece uma atenção especial e será detalhada em partes menores. Primeiro veremos o método set():

	public synchronized void set(int idProdutor, int valor) {
		while (disponivel == true) {
			try {
				System.out.println("Produtor #" + idProdutor + " esperando...");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		conteudo = valor;
		System.out.println("Produtor #" + idProdutor + " colocou " + conteudo);
		disponivel = true;
		notifyAll();
	}

Enquanto houver um conteúdo disponível no buffer, o produtor deve esperar. Quando o buffer não estiver mais com conteúdo disponível, o conteúdo é carregado e as outras thread são notificadas que um novo conteúdo está disponível.

	public synchronized int get(int idConsumidor) {
		while (disponivel == false) {
			try {
				System.out.println("Consumidor #" + idConsumidor
						+ " esperado...");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		System.out.println("Consumidor #" + idConsumidor + " consumiu: "
				+ conteudo);
		disponivel = false;
		notifyAll();
		return conteudo;
	}

De forma semelhante ao Produtor, no método get o consumidor deve esperar enquanto não houver conteúdo disponível. Quando o conteúdo estiver disponível novamente o consumidor consome o recurso e notifca as outras thread que não há mais recurso disponível.

O código completo da classe Buffer fica assim:

package br.concorrencia.produtorConsumidor;

public class Buffer {

	private int conteudo;
	private boolean disponivel;

	public synchronized void set(int idProdutor, int valor) {
		while (disponivel == true) {
			try {
				System.out.println("Produtor #" + idProdutor + " esperando...");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		conteudo = valor;
		System.out.println("Produtor #" + idProdutor + " colocou " + conteudo);
		disponivel = true;
		notifyAll();
	}

	public synchronized int get(int idConsumidor) {
		while (disponivel == false) {
			try {
				System.out.println("Consumidor #" + idConsumidor
						+ " esperado...");
				wait();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		System.out.println("Consumidor #" + idConsumidor + " consumiu: "
				+ conteudo);
		disponivel = false;
		notifyAll();
		return conteudo;
	}
}

O importante ai é verificar que os métodos get e set do buffer possuem um modificador especial: synchronized.

Segundo [4], métodos sincronizados são uma estratégia simples para prevenir erros de interferência e inconsistência de memória. Se um objeto é acessado por mais de uma thread (o que no nosso exemplo é o objeto Buffer), todas as operações de escrita e leitura neste objeto devem ser feitas através de métodos sincronizados.

Para testar o nosso código, vamos criar o seguinte método main:

	public static void main(String[] args) {
		Buffer bufferCompartilhado = new Buffer();
		Produtor produtor1 = new Produtor(1, bufferCompartilhado, 5);
		Produtor produtor2 = new Produtor(2, bufferCompartilhado, 5);
		Consumidor consumidor1 = new Consumidor(1, bufferCompartilhado, 2);
		Consumidor consumidor2 = new Consumidor(2, bufferCompartilhado, 8);

		produtor1.start();
		consumidor1.start();
		produtor2.start();
		consumidor2.start();
	}

Criamos dois produtores e dois consumidores. Em seguida executamos o método da classe Thread chamado start(). Este método vai então inicializar a Thread e logo em seguida vai executar o método run(), que nós definimos nas classes Consumidor e Produtor.

Observando a saída no console, temos algo do tipo:

Produtor #1 colocou 0
Produtor #1 esperando…
Consumidor #1 consumiu: 0
Consumidor #1 esperado…
Produtor #1 colocou 1
Produtor #1 concluido!
Consumidor #1 consumiu: 1
Consumidor #1 esperado…
Produtor #2 colocou 0
Produtor #2 esperando…
Consumidor #2 consumiu: 0
Consumidor #2 concluido!
Produtor #2 colocou 1
Produtor #2 concluido!
Consumidor #1 consumiu: 1
Consumidor #1 concluido!

A ordem de execução pode mudar pois a chamada aos Thread são feitas aleatoriamente em Java. Quando o método wait() na classe Buffer é executado, a Thread que está acessando ele fica em estado de espera. Quando o método notifyAll() é chamado, TODAS as Thread que estão em espera são acordadas e executadas, por isso definimos um laço while nos métodos get e set, ou seja, a Thread só será liberada quando a condição for verdadeira.

Uma discussão mais aprofundada sobre o assunto pode ser encontrada em [4]. No próximo post vamos ver alguns exemplos mais legais usando semáforos de diversos tipos para controlar o acesso ao recurso compartilhado.

Obrigado pela visita, espero que este post tenha ajudado. Se gostou do post compartilhe com seus amigos e colegas, senão, comente o que pode ser melhorado. Encontrou algum erro no código? Comente também. Possui alguma outra opinião ou alguma informação adicional? Comenta ai! 🙂

Referências:

[1] WIKIPEDIA. Monitor (concorrência). Disponível em: http://pt.wikipedia.org/wiki/Monitor_(concorrência). Acesso em: 23 set. 2011.
[2] MARTIN, Robert C. Clean code: a handbook of agile software craftsmanship.
[3] Sincronização de Threads. Disponível em: http://www.dsc.ufcg.edu.br/~jacques/cursos/map/html/threads/sincronizacao.html. Acesso em 23 set. 2011.
[4] ORACLE. Synchronized Methods. Disponível em: http://download.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html. Acesso em 23 set. 2011.

Anúncios

6 comentários sobre “Praticando concorrência em Java!

  1. Cara mto bom, porem colokei aki para testar utilizando seu exemplo em um aplicação e da erro exatamente no metodo run do produtor e consumidor em
    pilha.set(idProdutor, i); e pilha.get(idConsumidor);

    • Thiago, isso aconteceu porque vc importou a classe Buffer do pacote java.nio.Buffer importe do pacote da classe Buffer criada no exemplo.

  2. Obrigado, excelente material ajudou muito a entender.

  3. Ótimo Exemplo, mais o programa está dando erro nos métodos get e set das Classes Produtor e Consumidor além de estar sendo exigido um método que exceção dentro das respectivas classes, método esse que não é citado no seu exemplo.

    Método:

    Produtor(int i, Classes.Buffer bufferCompartilhado, int i0) {
    throw new UnsupportedOperationException(“Not supported yet.”); //To change body of generated methods, choose Tools | Templates.
    }

  4. Como faço no caso de o número de produtores ou consumidores variar de acordo com a entrada do usuário?
    eu pensei em um for(){
    consumidor[i].start()
    }
    mas preciso garantir que o Main aguarde a execução de todos os consumidores (exemplo), pois preciso calcular o tempo total da operação.
    quando eu faço um join logo em seguida, somente o primeiro consumidor é executado. Acredito que seja porque o join faz com que o Main aguarde o primeiro consumidor terminar, fazendo com que o Main não itere sobre o for.

    não sei como resolver. :/

    • Oi Jhonas. No seu caso você vai precisar de um código um pouco mais robusto. Realmente o for talvez não seja a melhor solução pois a ideia é que o código seja executado em paralelo né e o for é sequencial. Você precisa implementar alguma maneira de o consumidor sinalizar que completou, pra daí a Main conseguir esperar que todos finalizem.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s