Подтвердить что ты не робот

Кэш-память одного объекта в Java

скажем, у нас есть объект CountryList в нашем приложении, который должен вернуть список стран. Загрузка стран - это тяжелая операция, поэтому список следует кэшировать.

Дополнительные требования:

  • CountryList должен быть потокобезопасным
  • CountryList должен загружать ленивый (только по запросу)
  • CountryList должен поддерживать недействительность кэша
  • CountryList должен быть оптимизирован с учетом того, что кеш будет крайне недействительным

Я придумал следующее решение:

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

Что вы думаете об этом? Вы видите что-то плохое? Есть ли другой способ сделать это? Как я могу сделать это лучше? Должен ли я искать совершенно другое решение в этом случае?

Спасибо.

4b9b3361

Ответ 1

Спасибо всем ребятам, особенно пользователю gid, который дал эту идею.

Моя цель состояла в том, чтобы оптимизировать производительность для операции get(), считая операцию invalidate(), вызывается очень редко.

Я написал класс тестирования, который запускает 16 потоков, каждый вызов get() - работает миллион раз. С этим классом я профилировал некоторую реализацию на моей двухядерной машине.

Результаты тестирования

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) "Никакая синхронизация" не является потокобезопасной, но дает нам лучшую производительность, с которой мы можем сравнивать.

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

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

2) "Нормальная синхронизация" - довольно хорошая производительность, стандартная без проблем реализация

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

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

3) "с MapMaker" - очень низкая производительность.

Посмотрите мой вопрос вверху кода.

4) "с поставщиками .memoize" - хорошая производительность. Но, поскольку производительность такой же "нормальной синхронизации" нам нужно оптимизировать или просто использовать "Обычную синхронизацию".

См. ответ пользователя "gid" для кода.

5) "с оптимизированным memoize" - выполнение, сопоставимое с "никакой синхронизацией" - реализация, но потокобезопасная. Это тот, который нам нужен.

Кэш-класс: (Интерфейсы поставщика, используемые здесь, из Библиотеки коллекций Google, и у нее есть только один метод get(). См. 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;
        }
    }
}

Пример использования:

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");
    }
}

Ответ 2

Коллекции google фактически поставляют только вещь для такого рода вещей: Supplier

Ваш код будет выглядеть примерно так:

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

Ответ 3

Всякий раз, когда мне нужно что-то кэшировать, мне нравится использовать шаблон прокси. Выполнение этого шаблона предполагает разделение проблем. Ваш оригинал объект может быть связан с ленивой загрузкой. Объект вашего доверенного лица (или опекуна) может отвечать за проверку кеша.

Подробнее:

  • Определить класс CountryList, который является потокобезопасным, предпочтительно используя блоки синхронизации или другие блокировки semaphore.
  • Извлеките этот интерфейс класса в интерфейс CountryQueryable.
  • Определите другой объект, CountryListProxy, который реализует CountryQueryable.
  • Разрешить экземпляр CountryListProxy и разрешать только ссылку на него через его интерфейс.

Здесь вы можете вставить стратегию недействительности кэша в прокси-объект. Сохраните время последнего загрузки, а при следующем запросе на просмотр данных сравните текущее время с временем кеша. Определите уровень допуска, где, если прошло слишком много времени, данные будут перезагружены.

Что касается Lazy Load, обратитесь здесь.

Теперь для некоторого хорошего примерного примера:

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.
    }

}

Ответ 4

Я не уверен, для чего предназначена карта. Когда мне нужен ленивый, кешированный объект, я обычно делаю это так:

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;
  }
}

Я думаю, что это похоже на то, что вы делаете, но немного проще. Если у вас есть потребность в карте и ONE, которую вы упростили для вопроса, хорошо.

Если вы хотите, чтобы он был потокобезопасным, вы должны синхронизировать get и забыть.

Ответ 5

Что вы думаете об этом? Вы видите что-то плохое?

Bleah - вы используете сложную структуру данных MapMaker с несколькими функциями (доступ к карте, concurrency -другой доступ, отложенное построение значений и т.д.) из-за единственной функции, которой вы пользуетесь (отложенное создание одного строительно-дорогой объект).

В то время как повторное использование кода является хорошей целью, этот подход добавляет дополнительные накладные расходы и сложность. Кроме того, он вводит в заблуждение будущих сопровождающих, когда они видят структуру данных карты там, думая, что там есть карта ключей/ценностей, когда есть действительно только 1 вещь (список стран). Простота, удобочитаемость и ясность являются ключом к будущей ремонтопригодности.

Есть ли другой способ сделать это? Как я могу сделать это лучше? Должен ли я искать совершенно другое решение в этом случае?

Похоже, вы после ленивой загрузки. Посмотрите на решения других вопросов, связанных с ленивой загрузкой. Например, этот подход охватывает классический подход с двойной проверкой (убедитесь, что вы используете Java 1.5 или новее):

Как решить "Блокировка с двойной проверкой" "Разбита" ; Декларация на Java?

Вместо того, чтобы просто просто повторить здесь код решения, я думаю, что полезно прочитать дискуссию о ленивой загрузке через двойную проверку, чтобы расширить свою базу знаний. (извините, если это произойдет как помпезное - просто попробуйте научить ловить рыбу, а не кормить бла-бла-бла...)

Ответ 6

Существует библиотека (от atlassian) - один из классов использования, называемый LazyReference. LazyReference - это ссылка на объект, который можно лениво создать (сначала получить). он гарантированно защищен потоком, а init также должен выполняться только один раз - если два потока вызывает get() в одно и то же время, один поток будет вычисляться, другой поток блокирует ожидание.

см. пример кода:

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

Ответ 7

Ваши потребности здесь довольно просты. Использование MapMaker делает реализацию более сложной, чем она должна быть. Вся двунаправленная блокировка идиомы сложна, чтобы получить право, и работает только на 1.5+. И, честно говоря, он нарушает один из самых важных правил программирования:

Преждевременная оптимизация - это корень все зло.

Идентификатор блокировки с двойной проверкой пытается избежать затрат на синхронизацию в случае, когда кеш уже загружен. Но что накладные расходы действительно вызывают проблемы? Стоит ли стоить более сложного кода? Я утверждаю, что до тех пор, пока профилирование не сообщит вам об этом.

Здесь очень простое решение, которое не требует стороннего кода (игнорируя аннотацию JCIP). Он делает предположение, что пустой список означает, что кеш еще не загружен. Он также предотвращает переход из списка стран в код клиента, который может потенциально изменить возвращаемый список. Если это вас не беспокоит, вы можете удалить вызов 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();
        }
    }

}

Ответ 8

Это выглядит хорошо для меня (я предполагаю, что MapMaker - из коллекций google?) В идеале вам не нужно использовать карту, потому что у вас действительно нет ключей, но поскольку реализация скрыта от любых вызывающих абонентов, которых я не вижу это как большая сделка.

Ответ 9

Это простой способ использования материала ComputingMap. Вам нужна только простая реализация, где все методы синхронизированы, и все должно быть хорошо. Это, очевидно, блокирует первый поток, ударяющий по нему (получение), и любой другой поток, попавший в него, в то время как первый поток загружает кеш (и тот же самый, если кто-либо вызывает invalidateCache, где вы также должны решить, должен ли invalidateCache загружать кеш заново или просто исключить его, позволяя первой попытке получить его снова блокировать), но затем все потоки должны проходить через красиво.

Ответ 11

Следуйте за решением Майка выше. Мой комментарий не форматировался, как ожидалось...: (

Следите за проблемами синхронизации в работе B, тем более, что load() работает медленно:

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

    //Do whatever.
    return whatever;
}

Вы можете исправить это следующим образом:

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

    //Do whatever.
    return whatever;
}

Убедитесь, что вы ВСЕГДА синхронизированы при каждом доступе к загруженной переменной.