Threads

Threads permitem que várias atividades aconteçam simultaneamente em um mesmo programa. Como a programação multitarefa é mais complicada, caso exista uma classe de biblioteca pronta que poupe o trabalho da programação em baixo nível, utilize-a. Porém, mesmo usando essas bibliotecas onde aplicável, ainda será necessário trabalhar com código para multitarefa de vez em quando.

Item 48: Sincronize o acesso a dados mutáveis compartilhados
A palavra
synchronized garante que apenas uma thread irá executar certa parte do código de cada vez. Isso evita que um objeto seja observado em um estado inconsistente, enquanto ainda está sendo modificado por outra thread. Assim, o objeto é criado em um estado consistente e travado pelos métodos que o acessam. Esses métodos observam o estado e opcionalmente causam uma transição de estado, transformando o objeto de um estado consistente para outro. O uso apropriado de sincronização garante que nenhum método irá observar o objeto em estado inconsistente.
Além disso, a sincronização garante que um objeto progrida de um estado consistente para outro através de uma seqüência ordenada de transição de estados que parecem executar seqüencialmente. Cada thread entrando em um bloco sincronizado vê os efeitos de todas as transições de estado anteriores controladas pela mesma trava.
A linguagem garante que ler ou escrever em uma variável (exceto long ou double) é um processo atômico. Portanto, o valor retornado ao ler uma variável é, com certeza, o valor exato que foi gravado por alguma thread, ainda que threads múltiplas modifiquem a variável ao mesmo tempo sem sincronização. Porém, mesmo assim a sincronização é importante para garantir uma comunicação confiável entre as threads e conseguir a exclusão mútua. Veja como exemplo o seguinte código:
private static int nextSerialNumber = 0;
public static int generateSerialNumber() {
    return nextSerialNumber++;
}
O método acima não é confiável sem a sincronização, pois o operador de incremento (
++) lê e escreve no campo nextSerialNumber, portanto não é atômico. As operações de ler e escrever são independentes, executadas em seqüência. Múltiplas threads simultâneas podem observar o campo com o mesmo valor e portanto retornar o mesmo número serial. Além disso, é possível que uma thread chame o método generateSerialNumber
repetidamente, obtendo uma seqüência de números seriais de zero até n, e depois disso uma outra thread chame o generateSerialNumber e obtenha um número serial zero novamente, pois sem a sincronização a segunda thread pode não ver as atualizações feitas pela anterior devido ao modelo de memória do Java. Para consertar o código basta adicionar o modificador synchronized na declaração do método.
Para finalizar uma thread é recomendado que ela verifique regularmente uma variável (normalmente um booleano ou uma referência a um objeto) cujo valor indique quando ela deve parar. Apesar da leitura da variável ser atômica, também é necessária a sincronização neste caso, pois caso contrário não é garantido quando a thread irá ver a mudança no valor da variável causada por outra thread. Assim, é utilizado um método sincronizado para mudar o valor da variável e outro para verificar este valor, que pode retornar um booleano indicando se a thread deve ser finalizada. Note que neste caso a sincronização é usada apenas por seus efeitos na comunicação, não para a exclusão mútua, já que as ações de cada método são atômicas. O custo dessa sincronização é bem baixo, mas para um código mais limpo e uma pequena melhora de performance a sincronização pode ser omitida declarando o campo como
volatile, que garante que qualquer thread que leia o campo verá o valor mais recente.
No caso de não sincronizar corretamente o acesso a dados mutáveis o problema é ainda maior. Considere a técnica de double-check para lazy initialization:
private static Foo foo = null;
public static Foo getFoo() {
    if (foo == null) {
       synchronized (Foo.class) {
          if (foo == null)
             foo = new Foo();
       }
    }
    return foo;
}
A idéia desta técnica é evitar o custo da sincronização no caso mais comum, que é acessar o campo (
foo) depois dele ser inicializado. A sincronização é usada apenas para evitar que múltiplas threads inicializem o campo. Isso garante que a inicialização ocorra no máximo uma vez e que todas as threads chamando o getFoo irão obter o valor correto da referência ao objeto. Porém, a referência ao objeto pode não funcionar corretamente. Se uma thread lê a referência sem sincronização e então chama um método no objeto referenciado, o método pode observar o objeto em um estado intermediário da inicialização, o que pode causar sérios problemas. Isso pode acontecer porque apesar da referência só ser "publicada" no campo foo após o objeto ser inteiramente construído, sem a sincronização não é garantido que a thread verá todos os dados que foram armazenados na memória antes da publicação da referência do objeto. Em geral, ler a referência de um objeto não garante que a thread irá ver os valores mais recentes dos dados que constituem internamente o objeto referenciado, e portanto a técnica de double-check não funciona.
Para corrigir este problema há três alternativas: dispensar a lazy initialization instanciando o objeto diretamente, sincronizar o método
getFoo
ou utilizar a técnica initialize-on-demand holder class, que é apropriada quando um campo estático gasta muitos recursos para ser inicializado e pode não ser necessário, mas será usado intensivamente se for necessário. A técnica é mostrada abaixo:
private static class FooHolder {
    static final Foo foo = new Foo();
}
public static Foo getFoo() { return FooHolder.foo; }
Esta técnica explora o fato de que uma classe não é inicializada até que seja usada. Quando o método
getFoo é chamado pela primeira vez, ele lê o campo FooHolder.foo, fazendo com que a classe FooHolder seja inicializada. A beleza desta técnica é que o método getFoo não é sincronizado e executa apenas o acesso a um campo, não aumentando o seu custo. O único inconveniente é que esta técnica funciona apenas para campos estáticos, não para campos de instância.

Item 49: Evite uso excessivo de sincronização
Este item trata do problema oposto ao anterior. Dependendo da situação, sincronização excessiva pode causar redução de performance, deadlock ou até mesmo comportamento não determinístico.
Para evitar risco de deadlock, nunca ceda controle ao cliente dentro de um método ou bloco sincronizado. Em outras palavras, dentro de uma região sincronizada, não chame métodos public ou protected que foram feitos para serem sobrescritos, pois a classe que contém a região sincronizada não tem conhecimento de sua implementação e esta pode causar deadlock.
Portanto, coloque a chamada a métodos externos fora da região sincronizada. Essa técnica é chamada de open call. Além de prevenir deadlocks, open calls propiciam um aumento na simultaneidade. Assim o método pode rodar por um período longo durante o qual outras threads não terão acesso negado a objetos compartilhados com esse método.
Como regra geral, faça o mínimo possível dentro de regiões sincronizadas. Outro problema grave que pode acontecer é o método externo chamar de volta a região sincronizada, na mesma thread, quando seus dados internos estão em estado inconsistente. Como o mecanismo de travas é recursivo, isso não causa deadlock, e como a thread já possui a trava ela vai conseguir obter a trava uma segunda vez apesar de já existir uma operação não relacionada em progresso nos dados protegidos dentro do bloco sincronizado. Dessa forma, o mecanismo de trava torna-se inútil.
Além dos problemas na confiabilidade do código, o uso excessivo de sincronização pode diminuir a performance. De forma geral não é uma redução muito significativa e cai nas "pequenas otimizações" que devem ser evitadas segundo o Knuth (item 37), mas alguns casos devem ser analisados.
Se você estiver escrevendo uma abstração de baixo nível, que normalmente será usada por uma única thread ou como um componente em um objeto sincronizado maior, não costuma ser necessário sincronizar a classe internamente. Qualquer que seja sua decisão, é importante que você documente suas propriedades em relação à segurança de threads.
Mas se você estiver escrevendo uma classe que será muito usada em circunstâncias que requerem sincronização e também em circunstâncias em que ela não é necessária, uma abordagem razoável é prover os dois tipos, sincronizada (thread-safe) e não sincronizada (thread-compatible). Uma forma de fazer isso é criar uma classe wrapper (item 14) que implementa uma interface que descreve a classe e realiza a sincronização apropriada antes de realizar a chamada do método no objeto. Outra forma mais simples, que pode ser usada em classes que não serão estendidas ou reimplementadas, é prover uma classe não sincronizada e uma subclasse consistindo apenas em métodos sincronizados que chamam seus equivalentes na superclasse.
Uma boa razão para sincronizar uma classe internamente é quando ela será muito usada por threads simultâneas e é possível conseguir uma maior simultaneidade através de um ajuste fino na sincronização. Assim, ao invés de ter que sincronizar todo o objeto, apenas algumas partes serão sincronizadas internamente. Além disso, se uma classe ou método estático depende de um campo estático mutável é necessário fazer o sincronismo interno, pois pode não ser possível para o cliente realizar o sincronismo externamente porque não é garantido que os outros clientes farão o mesmo.

Item 50: Nunca chame o método
wait fora de um loop
O método
Object.wait
é usado  para fazer a thread esperar determinada condição. Ele precisa ser chamado dentro de uma região sincronizada que trava o objeto em que é chamado. Esta é a técnica padrão para usar o método wait:
synchronized (obj) {
    while (<condição não atendida>)
       obj.wait();
       // executa ação apropriada à condição
}
Sempre utilize-a para chamar o método
wait. O loop serve para testar a condição antes e depois do wait. Testar a condição antes de esperar e pular a espera se a condição já for atendida é necessário para garantir a vida da thread. Se a condição já tiver sido atendida e o método notify já tiver sido chamado antes do wait não há garantias de que a thread volte a ser executada. Testar a condição depois de esperar e esperar novamente se a condição não for atendida é importante para garantir a segurança, já que se a thread continuar sem que a condição tenha sido atendida ela pode destruir o estado protegido pela trava.
Outra questão relacionada é o uso de
notify ou notifyAll. De forma geral, use sempre o notifyAll. Este é o conselho mais conservador considerando que todas as chamadas ao wait estão dentro de loops, pois garante que as threads que precisam ser reativadas sempre serão reativadas. Dessa forma também serão reativadas outras threads, mas isso não influi no resultado do programa, já que será testada a condição antes de continuar qualquer execução.
Para otimizar, você pode decidir usar o
notify ao invés do notifyAll se todas as threads estão esperando a mesma condição e apenas uma thread de cada vez pode se beneficiar da condição se tornar verdadeira. Porém, mesmo assim pode ser recomendado utilizar o notifyAll, pois isto protege o código de uma chamada indevida ao wait por outra thread não relacionada, o que é particularmente importante se o objeto for público.
Por outro lado, ainda que o
notifyAll garanta que o código sempre executará corretamente, ele pode prejudicar a performance. Isso ocorre especialmente em certas estruturas de dados em que algumas threads possuem um status especial e as outras precisam esperar, o que torna quadrático o número de threads sendo tiradas do modo de espera. Se apenas algumas threads são elegíveis ao status especial, você deve usar o pattern
Specific Notification.

Item 51: Nunca dependa do agendador de threads
Quando múltiplas threads são executáveis, o agendador de threads determina qual irá rodar e por quanto tempo. A política que será utilizada para essa decisão varia muito de acordo com a implementação da JVM, portanto, para que o programa seja portável, é importante que ele não dependa do agendador de threads para executar corretamente ou ter uma boa performance. Para isso, uma aplicação multitarefa deve ter o mínimo de threads executáveis em qualquer momento.
A principal forma de atingir esse objetivo é possuir threads que executam uma quantidade de trabalho pequena e então esperam por alguma condição (
wait) ou pela passagem de certo intervalo de tempo (sleep). Deve ser evitado o busy-wait, em que a thread fica checando repetidamente certa estrutura de dados enquanto espera algo acontecer, pois isso pode aumentar muito a carga no processador.
Quando encontrar um programa que funciona mal porque algumas threads não recebem tempo de CPU suficiente em relação a outras, resista à tentação de consertar o programa incluindo chamadas ao
Thread.yield. Isso pode funcionar, mas tornará o programa não-portável do ponto de vista de performance. Outra técnica similar e ainda menos portável é o ajuste de prioridade das threads. Uma melhor abordagem é reestruturar o programa para reduzir o número de threads executáveis ao mesmo tempo.
O único uso válido para Thread.yield é para aumentar artificialmente a simultaneidade durante os testes, ajudando a explorar uma maior parte do programa.

Item 52: Documente a segurança em relação a threads
O comportamento de uma classe quando seus métodos estão sujeitos a uso simultâneo é uma parte importante do contrato que ela estabelece com seus clientes.
Para permitir seu uso seguro de forma multitarefa, a classe deve documentar claramente o nível de segurança que ela suporta, de acordo com a lista a seguir:
- immutable: instâncias desta classe parecem constantes para os clientes. Não é necessária sincronização externa.
- thread-safe: as instâncias são mutáveis, mas todos os métodos possuem sincronização interna suficiente. Chamadas simultâneas parecerão ser executadas serialmente, em uma ordem consistente.
- conditionally thread-safe: similar a anterior, porém alguns métodos precisam ser chamados em seqüência e sem interferência de outras threads. O cliente precisa obter a trava apropriada durante a execução da seqüência.
- thread-compatible: instâncias dessa classe podem ser usadas seguramente de forma simultânea se cada chamada de método (ou seqüência) estiver dentro de uma região sincronizada.
- thread-hostile: o uso dessa classe por múltiplas threads não é seguro, mesmo que todos os métodos sejam sincronizados externamente. Normalmente isso ocorre quando os métodos modificam dados estáticos que afetam outras threads.

Item 53: Evite grupos de threads
Junto com threads, travas e monitores, uma abstração básica oferecida pelo sistema de threads é a de grupos de threads. Eles servem para aplicar primitivos de
Thread para várias threads ao mesmo tempo. A maioria desses primitivos foi depreciada e os restantes são pouco usados, portanto grupos de threads não fornecem muita funcionalidade útil. A API ThreadGroup é fraca em termos de segurança de threads e possui diversas falhas. Como ela está obsoleta, não há previsões de que seja melhorada: ela foi um experimento mal sucedido que pode ser ignorado. Se você precisar escrever uma classe que lida com grupos lógicos de threads, armazene as referências em uma array ou collection. O único caso que justifica o uso de ThreadGroup
é o método ThreadGroup.uncaughtException, que é chamado automaticamente quando uma thread do grupo lança uma exceção que não é tratada. O comportamento padrão mostra a stack trace no standard error stream, mas você pode sobrescrever essa implementação.



Bibliografia:
Bloch, Joshua. Effective Java.

Resumo por: Vanessa Sabino
1