Solucionado (ver solução)
Solucionado
(ver solução)
7
respostas

Dúvida no Ex. 3 da Aula 6 - Threads

Estava fazendo uns testes aqui e observei que o resultado varia quando utilizo o método da forma abaixo:

public void run() {
        for (int i = comeco; i < fim; i++) {
            mensagens.add("Mensagem " + i);
        }
    }

Se eu utilizar da forma abaixo funciona corretamente ( sem exception).

public synchronized void run() {
        for (int i = comeco; i < fim; i++) {
            synchronized (mensagens) {
                mensagens.add("Mensagem " + i);
            }
        }
    }

Poderiam me explicar porque isto ocorre?

7 respostas

Fala Leandro, blz? Você poderia postar todo o código? Fica meio difícil sem ele completo. abraço!

    package br.com.empresa.banco;

    import java.util.Collection;

    public class ProduzMensagens implements Runnable {
        private int comeco;
        private int fim;
        private Collection<String> mensagens;

        public ProduzMensagens(int comeco, int fim, Collection<String> mensagens) {
            this.comeco = comeco;
            this.fim = fim;
            this.mensagens = mensagens;
        }

        public void run() {
            for (int i = comeco; i < fim; i++) {
                mensagens.add("Mensagem " + i);
            }
        }

    /*
        public void run() {
            for (int i = comeco; i < fim; i++) {
                synchronized (mensagens) {
                    mensagens.add("Mensagem " + i);
                }
            }
        }
    */

    }

    /////////////////////////////////////////////////////////////////////////////

    package br.com.empresa.banco;

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.LinkedList;
    import java.util.Vector;

    public class RegistroDeMensagens {
        public static void main(String[] args) throws InterruptedException {
            Collection<String> mensagens = new LinkedList<>();

            Thread t1 = new Thread(new ProduzMensagens(0, 10000, mensagens));
            Thread t2 = new Thread(new ProduzMensagens(10000, 20000, mensagens));
            Thread t3 = new Thread(new ProduzMensagens(20000, 30000, mensagens));

            t1.start();
            t2.start();
            t3.start();

            // faz com que a thread que roda o main aguarde o fim dessas
            t1.join();
            t2.join();
            t3.join();

            System.out.println("Threads produtoras de mensagens finalizadas!");

            // verifica se todas as mensagens foram guardadas
            for (int i = 0; i < 15000; i++) {
                if (!mensagens.contains("Mensagem " + i)) {
                    throw new IllegalStateException("não encontrei a mensagem: "
                            + i);
                }
            }

            // verifica se alguma mensagem ficou nula
            if (mensagens.contains(null)) {
                throw new IllegalStateException("não devia ter null aqui dentro!");
            }

            System.out.println("Fim da execucao com sucesso");
        }
    }

No segundo exemplo — o que dá certo —, tu fizeste o uso do modificador synchronized.

(obs: aliás, tu só precisas do segundo synchronized ali, o que recebe mensagens como argumento; o synchronized que consta na assinatura do método (run) é dispensável, e se for o único, causará exceções também).

Quando tu englobas a adição de novos elementos na coleção num bloco sincronizado, passando aquela coleção como argumento (synchronized(mensagens)), tu estás promovendo um acesso regrado àquela coleção; isto é, uma thread só poderá realizar operações naquela coleção quando uma outra thread já o tiver feito. Não ficará havendo alternâncias nas execuções — motivo pelo qual as exceções ocorrem nesse exemplo sem o synchronized.

Acabei me enrolando um pouco na hora de postar minha dúvida. Segue abaixo os métodos:

//funciona intermitentemente public synchronized void run() { for (int i = comeco; i < fim; i++) { mensagens.add("Mensagem " + i); } }

//sempre funciona public void run() { for (int i = comeco; i < fim; i++) { synchronized (mensagens) { mensagens.add("Mensagem " + i); } }

A minha dúvida neste caso é: Por que eles não funcionam da mesma forma?

Vamos começar por um conceito importante.

Os mecanismos de sincronização do Java se baseiam na ideia do lock (trava). Todo objeto em tempo de execução tem um lock associado a ele. O lock é um valor especial que só pode ser possuído por no máximo uma thread.

O que isso...

synchronized(mensagens) {
    // corpo do bloco
}

...significa é:

  • espere mensagens ser destravado;
  • trave mensagens;
  • execute o corpo;
  • destrave mensagens.

Agora vamos para o cerne da tua dúvida.

No primeiro caso — o que recorre em erro —, o código é:

public synchronized void run() {
    for(int i = comeco; i < fim; i++) {
        mensagens.add("Mensagem " + i);
    }
}

O que isso significa é que tu estás sincronizando o acesso ao método run daquela instância de ProduzMensagens.

Porque isso não funciona? Vamos para o código feito no método main:

Thread t1 = new Thread(new ProduzMensagens(0, 10000, mensagens));
Thread t2 = new Thread(new ProduzMensagens(10000, 20000, mensagens));
Thread t3 = new Thread(new ProduzMensagens(20000, 30000, mensagens));

Perceba que criamos três instâncias da classe ProduzMensagens, passando cada uma como argumento numa thread diferente.

Ou seja: no código do primeiro exemplo, a palavra reservada synchronized é inútil; o resultado é o mesmo se não a usarmos. Porque há três instâncias diferentes de ProduzMensagens, cada uma com seu método run.

O primeiro exemplo apenas faria sentido se todas as threads recebessem como argumento uma mesma instância de ProduzMensagens.

Importante: qual é a intenção com o synchronized nesse exercício? Sincronizar o acesso à coleção mensagens.

Por isso o segundo exemplo funciona:

public void run() {
    for(int i = comeco; i < fim; i++) {
        synchronized(mensagens) {
            mensagens.add("Mensagem " + i);
        }
    }
}

Passamos mensagens como argumento para synchronized, de modo que o lock desse objeto (mensagens) seja possuído por apenas uma thread de cada vez.

Assim, a adição de elementos em mensagens (mensagens.add) é realizada uma thread por vez. Se a thread t1 possui o lock do objeto mensagens, então só ela pode ter acesso. Quando a mesma liberar o lock, somente aí outra thread (t2) pode obtê-lo e ter acesso exclusivo ao objeto.

Caso contrário, mensagens seria acessada de forma intermitente pelas threads, levando à ocorrência das ArrayIndexOutOfBoundsException e IllegalStateException.

Espero que tenha sanado tuas dúvidas. ;)

Durante a explicação eu tinha entendido que estaria sincronizando o bloco inteiro no exemplo abaixo:

public synchronized void run() { for(int i = comeco; i < fim; i++) { mensagens.add("Mensagem " + i); } }

Pelo visto o funcionamento é diferente. Então não consigo entender qual é a utilidade de sincronizar o método inteiro.

solução!

Lembra do conceito de lock, o qual expliquei mais acima? Acho que sei o que te causa dúvida; vou reforçar abaixo.

Quando tu declaras synchronized na assinatura do método, tu travas a instância (objeto) toda.

Quando sincronizamos um bloco, não passamos o objeto como argumento para synchronized?

synchronized (mensagens) {
    // corpo do bloco
}

No caso de um método, é como se fizemos isso:

synchronized(this)

A boa prática dita que o ideal é sempre sincronizar blocos, não métodos; o exposto acima é um dos fatores.