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