Пишем кеш с определенным временем хранения объектов с использованием java.util.concurrent

Не так давно, мне выпала задача, написать кеш, который сам себя чистит по истечению некоторого определенного времени. Требования к нему были следующие:
  1. Легковесность
  2. Потокобезобасность

В общем-то и все. До написания данной задачи с java.util.concurrent дела не имел. На мысль использования этого пакета меня натолкнул один мой коллега, у которого было нечто подобное, но не соответствовало тому функционалу который был нужен. Итак, начнем:

В качестве ключа будет выступать внутренний класс, который помимо прямого назначения будет определять он является «живым» или его можно удалить с кеша, так как время его существования подошло к концу:

    private static class Key {

        private final Object key;
        private final long timelife;

        public Key(Object key, long timeout) {
            this.key = key;
            this.timelife = System.currentTimeMillis() + timeout;
        }

        public Key(Object key) {
            this.key = key;
        }

        public Object getKey() {
            return key;
        }

        public boolean isLive(long currentTimeMillis) {
            return currentTimeMillis < timelife;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Key other = (Key) obj;
            if (this.key != other.key && (this.key == null || !this.key.equals(other.key))) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 43 * hash + (this.key != null ? this.key.hashCode() : 0);
            return hash;
        }

        @Override
        public String toString() {
            return "Key{" + "key=" + key + '}';
        }
    }


Сам класс кеша сделаем параметризированным. Внутри нам потребуется контейнер-хранилище. java.util.concurrent.ConcurrentHashMap лучше всего подходит. Время хранения по-умолчанию сдалаем отдельным полем. Далее создадим java.util.concurrent.ScheduledExecutorService:

public class CacheUtil<K, V> {

    private ConcurrentHashMap<Key, V> globalMap = new ConcurrentHashMap<Key, V>();
    private long default_timeout;
    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
        @Override
            public Thread newThread(Runnable r) {
                Thread th = new Thread(r);
                th.setDaemon(true);
                return th;
            }
        });
}

Поток сделаем демоном, для того чтоб при завершении основного потока процесс, который чистит кеш так же завершался.

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

    public CacheUtil(long default_timeout)  throws Exception {
        if (default_timeout < 100) {
            throw new Exception("Too short interval for storage in the cache. Interval should be more than 10 ms");
        }
        default_timeout = default_timeout;
        scheduler.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                long current = System.currentTimeMillis();
                for (Key k : globalMap.keySet()) {
                    if (!k.isLive(current)) {
                        globalMap.remove(k);
                    }
                }
            }
        }, 1, default_timeout/5, TimeUnit.MILLISECONDS);
    }


Далее следует добавить методы для работы с кешом — и все готово к использованию. Вот полный код:

public class Caсhe<K, V> {

    private volatile ConcurrentHashMap<Key, V> globalMap = new ConcurrentHashMap<Key, V>();
    private long default_timeout;
    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
        @Override
            public Thread newThread(Runnable r) {
                Thread th = new Thread(r);
                th.setDaemon(true);
                return th;
            }
        });

    /**
     * @param default_timeout  количество милисекунд - время которое обьект будет кранится в кеше.
     */
    public Cache(long default_timeout) throws Exception {
        if (default_timeout < 10) {
            throw new Exception("Too short interval for storage in the cache. Interval should be more than 10 ms");
        }
        this.default_timeout = default_timeout;
        scheduler.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                long current = System.currentTimeMillis();
                for (Key k : globalMap.keySet()) {
                    if (!k.isLive(current)) {
                        globalMap.remove(k);
                    }
                }
            }
        }, 1, default_timeout/5, TimeUnit.MILLISECONDS);
    }

    /**
     * @param default_timeout количество милисекунд - время которое обьект будет кранится в кеше
     */
    public void setDefault_timeout(long default_timeout) throws Exception {
        if (default_timeout < 100) {
            throw new Exception("Too short interval for storage in the cache. Interval should be more than 10 ms");
        }
        this.default_timeout = default_timeout;
    }

    /**
     * Метод для вставки обьекта в кеш
     * Время зранения берётся по умолчанию
     * @param <K>
     * @param <V>
     * @param key ключ в кеше
     * @param data данные
     */
    public void put(K key, V data) {
        globalMap.put(new Key(key, default_timeout), data);
    }

    /**
     * Метод для вставки обьекта в кеш
     * @param <K>
     * @param <V>
     * @param key ключ в кеше
     * @param data данные
     * @param timeout время хранения обьекта в кеше в милисекундах
     */
    public void put(K key, V data, long timeout) {
        globalMap.put(new Key(key, timeout), data);
    }

    /**
     * получение значения по ключу
     * @param <K>
     * @param <V>
     * @param key ключ для поиска с кеша
     * @return Обьект данных храняшийся в кеше
     */
    public V get(K key) {
        return globalMap.get(new Key(key));
    }

    /**
     * удаляет все значения по ключу из кеша
     * @param <K>
     * @param key - ключ
     */
    public void remove(K key) {
        globalMap.remove(new Key(key));
    }

    /**
     * Удаляет все значения из кеша
     */
    public void removeAll() {
        globalMap.clear();
    }

    /**
     * Полностью заменяет весь существующий кеш.
     * Время хранения по умолчанию.
     * @param <K>
     * @param <V>
     * @param map Карта с данными
     */
    public void setAll(Map<K, V> map) {
        ConcurrentHashMap tempmap = new ConcurrentHashMap<Key, V>();
        for (Entry<K, V> entry : map.entrySet()) {
            tempmap.put(new Key(entry.getKey(), default_timeout), entry.getValue());
        }
        globalMap = tempmap;
    }

    /**
     * Добавляет к сущесвуещему кешу переданую карту
     * Время хранения по умолчанию.
     * @param <K>
     * @param <V>
     * @param map Карта с данными
     */
    public void addAll(Map<K, V> map) {
        for (Entry<K, V> entry : map.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    private static class Key {

        private final Object key;
        private final long timelife;

        public Key(Object key, long timeout) {
            this.key = key;
            this.timelife = System.currentTimeMillis() + timeout;
        }

        public Key(Object key) {
            this.key = key;
        }

        public Object getKey() {
            return key;
        }

        public boolean isLive(long currentTimeMillis) {
            return currentTimeMillis < timelife;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Key other = (Key) obj;
            if (this.key != other.key && (this.key == null || !this.key.equals(other.key))) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 43 * hash + (this.key != null ? this.key.hashCode() : 0);
            return hash;
        }

        @Override
        public String toString() {
            return "Key{" + "key=" + key + '}';
        }
    }
}
Поделиться публикацией

Похожие публикации

Комментарии 24

    0
    Как-раз пару дней назад похожую задачу на собеседовании решал.
      +3
      Это всё хорошо, но зачем очередной велосипед при наличии класса Cache в Google Guava, который делает ровно всё то же самое?
        +2
        Наверное для людей, которые хотят более подробно изучить тему кеширования и понимать, что происходит внутри.
          0
          Велосипед безусловно имеет право жить и работать, но только зачем очередной велосипед на Хабре??
          +5
          1. именование у вас не единообразно — где-то «currentTimeMillis», где-то «default_timeout».

          2. ну и оно у вас вообще компилируется?? В имени класса выпишете, через «с», а в конструкторе через «s»:
          public class Caсhe<K, V> {

          public Cashe(long default_timeout) throws Exception {

          3. Странный у вас hashCode(). В чем смысл того, что вы прибавляете «43*7» к hashCode() ключа?
          public int hashCode() {
          int hash = 7;
          hash = 43 * hash + (this.key != null? this.key.hashCode(): 0);
          return hash;
          }
            0
            1, 2. Прошу прощение, у меня были только исходники и планшет. Писал пост на планшете, немного опечатался, т.к. пришлось выкидывать много лишнего. Исправил.

            3. hashCode() — генерировала IDE.
              +1
              По сути весь hashCode() можно написать как return key.hashCode(); если всё же считать, что key != null (а именно так на мой взгляд и следует считать, и мб кидать из конструктора NPE или IllegalArgument).
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Мне нужно было быстро доставать объекты по ключу с потокобезопасной коллекции. Сразу же на ум пришел ConcurrentMap.
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Сначала не совсем понял. Замечание принято. Спасибо.
              +1
              Свежедобавленный элемент может быть тут же удален вследствие race condition между добавляющим и чистящим потоками, плохо!
                0
                Дополню другими замечаниями.

                public V get(K key)
                {
                return globalMap.get(new Key(key, default_timeout));
                }

                Зачем здесь параметр default_timeout передавать в конструктор? Создайте для Key конструктор с одним аргументом key и используйте его. Всё равно же сравнения не по таймауту делаются. И вообще как-то на каждый get по объекту создавать — не самая лучшая идея…

                Дублирование кода в методах put, setAll и addAll.

                Для методов

                public void setAll(Map<K, V> map) ...
                public void setAll(Map<K, V> map, long timeout) ...

                не нужно писать один и тот же код в реализации. Просто из первого метода вызывайте второй — иначе зачем вам жуткий копипаст? Для put и addAll аналогично.
                  0
                  Спасибо. Исправил. Опять же проблемой было написания поста на планшете. Обязуюсь в следующий раз писать без ошибок.
                  +1
                  Вообще сама идея сделать кеш с указываемым временем жизни для каждого объекта кеша малоудачна.

                  Пример:
                  Делаем default_timeout 3600000 (1 час)
                  Добавляем put(«key», 60000); // 1 минута
                  Но даже через 10 минут скорее всего данные будут жить, так как проверка проводится 1 раз в 20 минут.
                  Выход: добавить параметр shrinkerWorkInterval (как в JCS) или сделать время жизни всех ключей одинаковым (как в Guava)

                  Кроме того не радуют вызов
                  return globalMap.get(new Key(key, default_timeout));
                  Создание нового объекта при каждом получении объекта из кеша? Выше уже посоветовали хранить данные в ConcurrentHashMap и отдельно данные о времени жизни в другой коллекции.
                    +1
                    Как по мне — поток проверки совершенно лишний.
                    Можно просто переопределить get и в момент запроса просто сравнивать время в ключе с текущим. Даже удалять не надо — просто возвращаете null, если time-to-live уже прошел.
                      0
                      И не запрашиваемые данные будут висеть в кеше пока уборщица не выдернет кабель питания?
                        0
                        Ну, удаляйте. Это уже ваше дело как себя вести с этими объектами. Может у вас будет обновление кеша по тем же ключам. Тогда удалять смысла нет — только лишняя операция.
                        Суть моего коммента была в том, что демон немножко бесполезен)
                          0
                          На это каждый смотрит со своей колокольни. Если в кеш будет вставка во много потоков больших объектов и обращение к этим объектам будет идти раз в пятилетку, то очень скоро мы получим OutOfMemory. А с демоном, если обращений совсем не будет то через некоторое время CHM станет пустой.
                            +1
                            Согласен, что надо смотреть под конкретную ситуацию, чтобы соблюсти баланс между потребляемой памятью и операциями над этим всем.
                            Не согласен про раз в пятилетку. На то он и кеш, что чтение многократно преобладает над изменением. Иначе — зачем он тогда вообще нужен.
                              0
                              Так этож кэш. Тут можно и SoftReference заюзать, что бы oome не получть*
                                0
                                Можно, но его уже нельзя будет назвать с определенным временем хранения. Не будет никакой гарантии, что объект пролежит в кеше те же 10 мин. т.к. при нехватки памяти GC их подметет.
                                  0
                                  Я считаю, что плох тот кэш, благодаря которому, можно OOME словить. Хотя, думаю, в любом случае, до OOME далеко. До него еще будут несколько сборок мусора и тормоза такие (если хип у нас достаточно большой), что польза от данного кэша быстро убежит в минус бесконечности.

                                  Одним словом я — за стабильность. Поэтому лучше кэш весь слить, нежели вообще сервак положить. Если слив кэша равносилен тому, что через несколько мгновений все ляжет и так — то надо думать как исправить эту ситуацию.
                        0
                        Лучше вместо того чтобы поток ходил по всем элементам в фоне, сделать кучу по времени жизни, и засыпать до момента когда нужно будет изъять ближайший.
                        При вставке элемента посылать потоку-уборщику interrupt(чтобы узнать что в вершине кучи мог появится элемент который нужно удалить раньше).

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое