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

в 20:56, , рубрики: concurrency, java, кеш, метки: , ,

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

  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 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 Cashe(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, default_timeout));
    }

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

    /**
     * Удаляет все значения из кеша
     */
    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 Карта с данными
     * @param timeout
     */
    public void setAll(Map<K, V> map, long timeout) {
        ConcurrentHashMap<Key, V> tempmap = new ConcurrentHashMap<Key, V>();
        for (Entry<K, V> entry : map.entrySet()) {
            tempmap.put(new Key(entry.getKey(), 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()) {
            globalMap.put(new Key(entry.getKey(), default_timeout), entry.getValue());
        }
    }

    /**
     * Добавляет к сущесвуещему кешу переданую карту
     * с заданым временем хранения
     * @param <K>
     * @param <V>
     * @param map Карта с данными
     * @param timeout 
     */
    public void addAll(Map<K, V> map, long timeout) {
        for (Entry<K, V> entry : map.entrySet()) {
            globalMap.put(new Key(entry.getKey(), timeout), 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 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 + '}';
        }
    }
}

Автор: Odis

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js