Потокобезопасный кеш одного объекта в Java - PullRequest
37 голосов
/ 03 сентября 2010

допустим, у нас есть объект 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);
    }
}

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

Спасибо.

Ответы [ 11 ]

33 голосов
/ 03 сентября 2010

Google коллекции на самом деле поставляет только вещь для такого рода вещи: Поставщик

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

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);
}
17 голосов
/ 06 сентября 2010

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

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

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

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

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

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

5) "с оптимизированной памятью" - производительность, сравнимая с реализацией "без синхронизации", но поточно-ориентированная.Это то, что нам нужно.

Сам класс кеша: (Используемые здесь интерфейсы поставщика взяты из библиотеки коллекций Google и имеют только один метод get (). См. http://google -коллекции.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");
    }
}
5 голосов
/ 03 сентября 2010

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

Подробно:

  • Определить объект CountryList класса, который является потокобезопасным, предпочтительно с использованием блоков синхронизации или других семафоров блокировок.
  • Извлечение интерфейса этого класса в интерфейс 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.
    }

}
1 голос
/ 03 сентября 2010

Ваши потребности здесь кажутся довольно простыми. Использование 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();
        }
    }

}
1 голос
/ 03 сентября 2010

Существует библиотека (от 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();
1 голос
/ 03 сентября 2010

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

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

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

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

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

Как решить декларацию «Двойная проверка блокировки сломана» в Java?

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

1 голос
/ 03 сентября 2010

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

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

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

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

0 голосов
/ 03 сентября 2010

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

Остерегайтесь проблем с синхронизацией в операции B, особенно из-за медленной загрузки ():

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

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

0 голосов
/ 03 сентября 2010

Использование Инициализация владельца по требованию

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 голосов
/ 03 сентября 2010

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

...