ti-enxame.com

Cache seguro de thread de um objeto em java

digamos que temos um objeto CountryList em nosso aplicativo que deve retornar a lista de países. O carregamento de países é uma operação pesada, portanto a lista deve ser armazenada em cache.

Requisitos adicionais:

  • O CountryList deve ser seguro para threads
  • O CountryList deve carregar preguiçosamente (somente sob demanda)
  • CountryList deve suportar a invalidação do cache
  • O CountryList deve ser otimizado, considerando que o cache será invalidado muito raramente

Eu vim com a seguinte solução:

public class CountryList {
    private static final Object ONE = new Integer(1);

    // MapMaker is from Google Collections Library    
    private Map<Object, List<String>> cache = new MapMaker()
        .initialCapacity(1)
        .makeComputingMap(
            new Function<Object, List<String>>() {
                @Override
                public List<String> apply(Object from) {
                    return loadCountryList();
                }
            });

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        return cache.get(ONE);
    }

    public void invalidateCache() {
        cache.remove(ONE);
    }
}

O que você acha disso? Você vê algo ruim nisso? Existe outra maneira de fazer isso? Como posso melhorar? Devo procurar totalmente outra solução nesses casos?

Obrigado.

37
Igor Mukhin

Obrigado a todos , especialmente ao usuário " gid " que deu o idéia.

Meu objetivo era otimizar o desempenho da operação get (), considerando que a operação invalidate () será chamada de muito rara.

Eu escrevi uma classe de teste que inicia 16 threads, cada chamada get () - Operação um milhão de vezes. Com esta classe, criei um perfil de alguma implementação no meu maschine de 2 núcleos.

Resultados dos testes

Implementation              Time
no synchronisation          0,6 sec
normal synchronisation      7,5 sec
with MapMaker               26,3 sec
with Suppliers.memoize      8,2 sec
with optimized memoize      1,5 sec

1) "Sem sincronização" não é seguro para threads, mas oferece o melhor desempenho com o qual podemos comparar.

@Override
public List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public void invalidateCache() {
    cache = null;
}

2) "Sincronização normal" - desempenho muito bom, implementação padrão simples

@Override
public synchronized List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public synchronized void invalidateCache() {
    cache = null;
}

3) "com o MapMaker" - desempenho muito ruim.

Veja minha pergunta no topo para o código.

4) "com Suppliers.memoize" - bom desempenho. Mas, como o desempenho da mesma "sincronização normal", precisamos otimizá-la ou apenas usar a "sincronização normal".

Veja a resposta do usuário "gid" para o código.

5) "com memorização otimizada" - o desempenho comparável à implementação "sem sincronização", mas com segurança para threads. É disso que precisamos.

A própria classe de cache: (as interfaces do fornecedor usadas aqui são da Biblioteca de coleções do Google e possuem apenas um método get (). Consulte http://google-collections.googlecode.com/svn/trunk/javadoc/ com/google/common/base/Supplier.html )

public class LazyCache<T> implements Supplier<T> {
    private final Supplier<T> supplier;

    private volatile Supplier<T> cache;

    public LazyCache(Supplier<T> supplier) {
        this.supplier = supplier;
        reset();
    }

    private void reset() {
        cache = new MemoizingSupplier<T>(supplier);
    }

    @Override
    public T get() {
        return cache.get();
    }

    public void invalidate() {
        reset();
    }

    private static class MemoizingSupplier<T> implements Supplier<T> {
        final Supplier<T> delegate;
        volatile T value;

        MemoizingSupplier(Supplier<T> delegate) {
            this.delegate = delegate;
        }

        @Override
        public T get() {
            if (value == null) {
                synchronized (this) {
                    if (value == null) {
                        value = delegate.get();
                    }
                }
            }
            return value;
        }
    }
}

Exemplo de uso:

public class BetterMemoizeCountryList implements ICountryList {

    LazyCache<List<String>> cache = new LazyCache<List<String>>(new Supplier<List<String>>(){
        @Override
        public List<String> get() {
            return loadCountryList();
        }
    });

    @Override
    public List<String> list(){
        return cache.get();
    }

    @Override
    public void invalidateCache(){
        cache.invalidate();
    }

    private List<String> loadCountryList() {
        // this should normally load a full list from the database,
        // but just for this instance we mock it with:
        return Arrays.asList("Germany", "Russia", "China");
    }
}
17
Igor Mukhin

as coleções do Google fornecem exatamente o que é preciso para esse tipo de coisa: Fornecedor

Seu código seria algo como:

private Supplier<List<String>> supplier = new Supplier<List<String>>(){
    public List<String> get(){
        return loadCountryList();
    }
};


// volatile reference so that changes are published correctly see invalidate()
private volatile Supplier<List<String>> memorized = Suppliers.memoize(supplier);


public List<String> list(){
    return memorized.get();
}

public void invalidate(){
    memorized = Suppliers.memoize(supplier);
}
33
Gareth Davis

Sempre que preciso armazenar algo em cache, gosto de usar o padrão de proxy . Fazer isso com esse padrão oferece uma separação de preocupações. Seu objeto original pode estar preocupado com o carregamento lento. Seu objeto proxy (ou tutor) pode ser responsável pela validação do cache.

Em detalhe:

  • Defina uma classe CountryList do objeto que seja segura para threads, de preferência usando blocos de sincronização ou outros bloqueios semáforo .
  • Extraia a interface desta classe para uma interface CountryQueryable.
  • Defina outro objeto, CountryListProxy, que implementa o CountryQueryable.
  • Permita apenas que o CountryListProxy seja instanciado e apenas faça referência a ele através de sua interface.

A partir daqui, você pode inserir sua estratégia de invalidação de cache no objeto proxy. Salve a hora do último carregamento e, na próxima solicitação para ver os dados, compare a hora atual com a hora do cache. Defina um nível de tolerância em que, se houver muito tempo, os dados serão recarregados.

Quanto ao Lazy Load, consulte aqui .

Agora, para obter um bom código de exemplo em casa:

public interface CountryQueryable {

    public void operationA();
    public String operationB();

}

public class CountryList implements CountryQueryable {

    private boolean loaded;

    public CountryList() {
        loaded = false;
    }

    //This particular operation might be able to function without
    //the extra loading.
    @Override
    public void operationA() {
        //Do whatever.
    }

    //This operation may need to load the extra stuff.
    @Override
    public String operationB() {
        if (!loaded) {
            load();
            loaded = true;
        }

        //Do whatever.
        return whatever;
    }

    private void load() {
        //Do the loading of the Lazy load here.
    }

}

public class CountryListProxy implements CountryQueryable {

    //In accordance with the Proxy pattern, we hide the target
    //instance inside of our Proxy instance.
    private CountryQueryable actualList;
    //Keep track of the lazy time we cached.
    private long lastCached;

    //Define a tolerance time, 2000 milliseconds, before refreshing
    //the cache.
    private static final long TOLERANCE = 2000L;

    public CountryListProxy() {
            //You might even retrieve this object from a Registry.
        actualList = new CountryList();
        //Initialize it to something stupid.
        lastCached = Long.MIN_VALUE;
    }

    @Override
    public synchronized void operationA() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }
    }

    @Override
    public synchronized String operationB() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }

        return whatever;
    }

}

public class Client {

    public static void main(String[] args) {
        CountryQueryable queryable = new CountryListProxy();
        //Do your thing.
    }

}
5
Mike

Não sei para que serve o mapa. Quando preciso de um objeto em cache e lento, geralmente faço assim:

public class CountryList
{
  private static List<Country> countryList;

  public static synchronized List<Country> get()
  {
    if (countryList==null)
      countryList=load();
    return countryList;
  }
  private static List<Country> load()
  {
    ... whatever ...
  }
  public static synchronized void forget()
  {
    countryList=null;
  }
}

Eu acho que isso é semelhante ao que você está fazendo, mas um pouco mais simples. Se você precisa do mapa e do UM que simplificou para a pergunta, tudo bem.

Se você deseja que ele seja seguro para threads, sincronize o get e o forget.

1
Jay

O que você acha disso? Você vê algo ruim nisso?

Bleah - você está usando uma estrutura de dados complexa, o MapMaker, com vários recursos (acesso ao mapa, acesso compatível com simultaneidade, construção adiada de valores etc.) devido a um único recurso que você está procurando (criação adiada de um único objeto caro de construção) .

Embora a reutilização de código seja uma boa meta, essa abordagem adiciona sobrecarga e complexidade adicionais. Além disso, isso engana futuros mantenedores quando eles veem uma estrutura de dados de mapas pensando que há um mapa de chaves/valores lá quando há realmente apenas uma coisa (lista de países). Simplicidade, legibilidade e clareza são essenciais para a manutenção futura.

Existe outra maneira de fazer isso? Como posso melhorar? Devo procurar totalmente outra solução nesses casos?

Parece que você está após o carregamento lento. Veja soluções para outras questões SO carregamento lento). Por exemplo, esta aborda a abordagem clássica de verificação dupla (verifique se você está usando Java 1.5 ou mais tarde):

Como resolver a declaração "O bloqueio com verificação dupla está quebrado" em Java?

Em vez de simplesmente repetir o código da solução aqui, acho útil ler a discussão sobre carregamento lento por meio de uma verificação dupla para aumentar sua base de conhecimento. (desculpe se isso parecer pomposo - tente ensinar a pescar em vez de alimentar blá blá blá ...)

1
Bert F

Existe uma biblioteca por aí (de atlassian ) - uma das classes util chamadas LazyReference . LazyReference é uma referência a um objeto que pode ser criado preguiçosamente (no primeiro get). ele é garantido como thread seguro, e o init também é garantido para ocorrer apenas uma vez - se duas chamadas de threads get () ao mesmo tempo, um thread será computado, o outro thread bloqueará a espera.

veja um código de exemplo :

final LazyReference<MyObject> ref = new LazyReference() {
    protected MyObject create() throws Exception {
        // Do some useful object construction here
        return new MyObject();
    }
};

//thread1
MyObject myObject = ref.get();
//thread2
MyObject myObject = ref.get();
1
Chii

Suas necessidades parecem bem simples aqui. O uso do MapMaker torna a implementação mais complicada do que precisa ser. Todo o idioma de bloqueio com verificação dupla é difícil de acertar e funciona apenas com mais de 1.5. E para ser sincero, está quebrando uma das regras mais importantes da programação:

Otimização prematura é a raiz de todo o mal.

O idioma de bloqueio verificado duas vezes tenta evitar o custo da sincronização no caso em que o cache já está carregado. Mas essa sobrecarga está realmente causando problemas? Vale a pena o custo de código mais complexo? Eu digo suponha que não seja até que o perfil diga o contrário.

Aqui está uma solução muito simples que não requer código de terceiros (ignorando a anotação JCIP). Ele assume que uma lista vazia significa que o cache ainda não foi carregado. Também evita que o conteúdo da lista de países escape para o código do cliente que poderia modificar a lista retornada. Se isso não for um problema para você, você poderá remover a chamada para Collections.unmodifiedList ().

public class CountryList {

    @GuardedBy("cache")
    private final List<String> cache = new ArrayList<String>();

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        synchronized (cache) {
            if( cache.isEmpty() ) {
                cache.addAll(loadCountryList());
            }
            return Collections.unmodifiableList(cache);
        }
    }

    public void invalidateCache() {
        synchronized (cache) {
            cache.clear();
        }
    }

}
1
wolfcastle

Isso parece bom para mim (presumo que o MapMaker seja de coleções do Google?) Idealmente, você não precisaria usar um mapa porque realmente não possui chaves, mas como a implementação está oculta a todos os chamadores, não vejo isso como um grande negócio.

0
Mike Q

Siga a solução de Mike acima. Meu comentário não foi formatado como esperado ... :(

Cuidado com os problemas de sincronização na operaçãoB, especialmente porque o load () é lento:

public String operationB() {
    if (!loaded) {
        load();
        loaded = true;
    }

    //Do whatever.
    return whatever;
}

Você pode corrigi-lo desta maneira:

public String operationB() {
    synchronized(loaded) {
        if (!loaded) {
            load();
            loaded = true;
        }
    }

    //Do whatever.
    return whatever;
}

Certifique-se de que SEMPRE sincronize todos os acessos à variável carregada.

0
romacafe

Esta é uma maneira simples de usar o material do ComputingMap. Você só precisa de uma implementação simples e simples, onde todos os métodos são sincronizados, e você deve estar bem. Obviamente, isso bloqueará o primeiro thread que o atinge (obtém) e qualquer outro thread que o atinge enquanto o primeiro thread carrega o cache (e o mesmo novamente se alguém chamar a coisa invalidateCache - onde você também deve decidir se o invalidateCache deve carregar o armazenar em cache novamente ou apenas anulá-lo, deixando a primeira tentativa de recuperá-lo novamente), mas todos os threads devem passar bem.

0
stolsvik

Use o Idioma do detentor da inicialização sob demanda

public class CountryList {
  private CountryList() {}

  private static class CountryListHolder {
    static final List<Country> INSTANCE = new List<Country>();
  }

  public static List<Country> getInstance() {
    return CountryListHolder.INSTANCE;
  }

  ...
}
0
helpermethod