|
|
Paulo Silveira
No java2, as operações nas novas coleções não são sincronizadas. Com isso, alguns novos termos são frequentemente citados: fail-fast, Concurrent Modification e outros. O que são?
Introdução
Este artigo é um pouco teórico, e consideramos que você tenha um bom conhecimento de Threads e sincronização, e familiaridade com as Collections. Ele propõe mostrar como funcionam as coleções "novas" (do java 1.2 em diante), que não são sincronizadas, rodando em multi thread.
Isto é, queremos mostrar porque os iterators destas coleções são conhecidos como "fail-fast" e qual o motivo de terem feito assim. Vamos mostrar também como sincronizar estas coleções, ou até mesmo como criar uma nova coleção sem o iterator "fail-fast".
Coleções Sincronizadas
Acho que a maioria já sabe: as antigas Collections (Hashtable, Vecotr, Stack, etc..) eram completamente sincronizadas. Isto é. você não enferentava problemas quando estava utilizando elas em uma situação multi-thread, mas em compensação, em ambiente single-thread, elas são extremamente lentas.
Quando veio o java2, com a collections framework, eles resolveram adicionar as novas collections não sincronizadas. Porque? Pois as sincronizadas, quando utilizadas em um modelo single thread (isto é, apenas uma thread acessando a coleção, fazendo a sincronização desnecessária), eram 9 vezes mais lentas do que se não fossem sincronizadas! Com isto dá para perceber o custo para se adiquirir um lock, mesmo ele estando disponível!
Mas, o que acontece quando você usa uma coleção não sincrona em ambiente multi thread? Se você não tem controle nenhum sobre a sincronização, os efeitos mais estranhos podem acontecer. Mexer em variáveis compartilhadas ao mesmo tempo, como o índice do array, pode fazer com que você perca dados, sobreescrevendo-os.
Para detectar tal anormalidade, as novas coleções utilizam os "fail-fast" iterators.
Fail-fast?
Este é um ponto que, na minha opinião pessoal, é muito mal escrito na documentação. Ou pelo menos ele deveria falar como isto é feito.
De acordo com a documentação: "Os iteradores desta classe são fail-fast. Se a coleção é modificada estruturalment depois da criação do iterador, que não seja pelos próprios métodos do iterador, o iterador ira lançar uma ConcurrentModificationException".
Então, se você pegar um iterador, e começar a chamar os next, e uma outra Thread estiver adicionando ou removendo elementos desta coleção, o iterador irá (as vezes) perceber, e lançará uma ConcurrentModificationException, que é uma RuntimeException.
Por quê isso? Desta maneira, se você estiver utilizando esta coleção concorrentemente, isto é, chamando ela por mais de uma thread ao mesmo tempo, o iterador irá lançar esta exceção, indicando que uma sincronização está, provavelmente mas não necessariamente, fazendo falta!
Vamos ver um dos iteradores como exemplo: o código fonte do iterador do java.util.AbstractList, que é uma classe interna da própria AbstractList:
01 public Object next() {
02 try {
03 Object next = get(cursor);
04 checkForComodification();
05 lastRet = cursor++;
06 return next;
07 } catch(IndexOutOfBoundsException e) {
08 checkForComodification();
09 throw new NoSuchElementException();
10 }
11 }
|
Como você ve, a todo momento, o iterador faz uma chamada para checkForComodification, e isto não é só no next. Vamos ver o código do checkForComodification.
1 final void checkForComodification() {
2 if (modCount != expectedModCount)
3 throw new ConcurrentModificationException();
4 }
|
A variável modCount é uma variável da AbstractList, que é incrementada toda vez que uma mudança estrutural acontece na coleção, isto é: adicionar, remover, etc...
A variável expectedModCount tem o valor da modCount no momento que o iterador é construído. Esta é uma forma de sincronização bem precária: toda vez que o iterador é chamado para alguma coisa, ele verifica se não houve uma mudança na coleção. Para isso, ele faz o if do trecho acima! Se houve uma alteração não esperada, ele já lança a exceção, afim de evitar maiores danos!
Agora entendemos claramente como o iterador sabe se houve uma mudança na lista a qual ele itera! Isto nos ajuda a perceber que deveríamos ter sincronizado algo e não sincronizamos! Se não houvesse esta proteção, quando você esquecesse de sincronizar, iria ter dificuldades de perceber que o bug do seu programa é esse, já que a exceção gerada seria alguns NullPointers e IndexArrayOutOfBounds.
Nota: Se você quer criar uma coleção mais rápida, crie uma classe iteradores que não sejam fast fail, mas vai ser difícil você perceber que está usando-os concorrentemente, já que as exceções que irão aparecer não vão indicar muito bem que este é o problema!
Sincronizando com a java.util.Collections
Você pode utilizar a classe auxiliar java.util.Collections para sincronizar as suas coleções que não tem sincronismo. Você vai utilizar um wrapper, que delega as chamadas para a sua antiga coleção, mas sempre dentro de um bloco sincronizado no this!
1 ArrayList al = new ArrayList();
2 List list = Collections.synchronizedList(al);
|
Agora, todo acesso a array será feito sincronizado com um mutex, garantindo exclusão mútua.
A partir deste momento, quando você chamar o método add, ele irá fazer o seguinte:
1 public boolean add(Object o) {
2 synchronized(mutex) {
3 return c.add(o);
4 }
5 }
|
O mutex aqui vale this, isto é, está sincronizado usando a própria coleção, que é o equivalente (quase) a sincronizar o método.
Com isso, você fica livre dos problemas de sincronia, mas os iterators não! Então você mesmo precisa sincroniza-los!
1 ArrayList al = new ArrayList();
2 List list = Collections.synchronizedList(al);
3
4 synchrinized(this) {
5 Iterator i = al.iterator();
6 while (i.hasNext()) {
7 i.next();
8 }
9 }
|
Porque os iterators não vem sincronizados automaticamente? Pois as classes que são iterators, são classes internas das respectivas Collections!
Se você não sincronizar os iteradores, você corre altos riscos de receber a ConcurrentModificationException se outro iterador estiver mexendo estruturalmente na coleção.
Conclusão
Espero que você tenha entendido o que são os iteradores fail fast, e porque os criaram assim. Evite sempre utilizar as coleções sincronizadass.
Mesmo quando você precisar de sincronização, prefira sincronizar você mesmo, "na mão". Assim você evita sincronização onde não há necessidade, que já vimos ter um custo alto.
|
|
|