/* * Copyright 2012 Thomas Bocek * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package net.tomp2p.utils; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A map with expiration and more or less LRU. Since the maps are separated in segments, the LRU is done for each * segment. A segment is chosen based on the hash of the key. If one segments is more loaded than another, then an entry * of the loaded segment may get evicted before an entry used least recently from an other segment. The expiration is * done best effort. There is no thread checking for timed out entries since the cache has a fixed size. Once an entry * times out, it remains in the map until it either is accessed or evicted. A test showed that for the default entry * size of 1024, this map has a size of 967 if 1024 items are inserted. This is due to the segmentation and hashing. * * @author Thomas Bocek * @param <K> * the type of the key * @param <V> * the type of the value */ public class ConcurrentCacheMap<K, V> implements ConcurrentMap<K, V> { private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentCacheMap.class); /** * Number of segments that can be accessed concurrently. */ public static final int SEGMENT_NR = 16; /** * Max. number of entries that the map can hold until the least recently used gets replaced */ public static final int MAX_ENTRIES = 1024; /** * Time to live for a value. The value may stay longer in the map, but it is considered invalid. */ public static final int DEFAULT_TIME_TO_LIVE = 60; private final CacheMap<K, ExpiringObject>[] segments; private final int timeToLiveSeconds; private final boolean refreshTimeout; private final AtomicInteger removedCounter = new AtomicInteger(); /** * Creates a new instance of ConcurrentCacheMap using the default values and a {@link CacheMap} for the internal * data structure. */ public ConcurrentCacheMap() { this(DEFAULT_TIME_TO_LIVE, MAX_ENTRIES, true); } /** * Creates a new instance of ConcurrentCacheMap using the supplied values and a {@link CacheMap} for the internal * data structure. * * @param timeToLiveSeconds * The time-to-live value (seconds) * @param maxEntries * The maximum number of entries until items gets replaced with LRU */ public ConcurrentCacheMap(final int timeToLiveSeconds, final int maxEntries) { this(timeToLiveSeconds, maxEntries, true); } /** * Creates a new instance of ConcurrentCacheMap using the supplied values and a {@link CacheMap} for the internal * data structure. * * @param timeToLiveSeconds * The time-to-live value (seconds) * @param maxEntries * The maximum number of entries until items gets replaced with LRU * @param refreshTimeout * If set to true, timeout will be reset in case of {@link #putIfAbsent(Object, Object)} */ @SuppressWarnings("unchecked") public ConcurrentCacheMap(final int timeToLiveSeconds, final int maxEntries, final boolean refreshTimeout) { this.segments = new CacheMap[SEGMENT_NR]; final int maxEntriesPerSegment = maxEntries / SEGMENT_NR; for (int i = 0; i < SEGMENT_NR; i++) { // set updateOnInsert to true, since it should behave as a regular map segments[i] = new CacheMap<K, ExpiringObject>(maxEntriesPerSegment, true); } this.timeToLiveSeconds = timeToLiveSeconds; this.refreshTimeout = refreshTimeout; } /** * Returns the segment based on the key. * * @param key * The key where the hash code identifies the segment * @return The cache map that corresponds to this segment */ private CacheMap<K, ExpiringObject> segment(final Object key) { return segments[(key.hashCode() & Integer.MAX_VALUE) % SEGMENT_NR]; } @Override public V put(final K key, final V value) { final ExpiringObject newValue = new ExpiringObject(value, System.currentTimeMillis()); final CacheMap<K, ExpiringObject> segment = segment(key); ExpiringObject oldValue; synchronized (segment) { oldValue = segment.put(key, newValue); } if (oldValue == null || oldValue.isExpired()) { return null; } return oldValue.getValue(); } @Override /** * This does not reset the timer! */ public V putIfAbsent(final K key, final V value) { final CacheMap<K, ExpiringObject> segment = segment(key); final ExpiringObject newValue = new ExpiringObject(value, System.currentTimeMillis()); ExpiringObject oldValue = null; synchronized (segment) { if (!segment.containsKey(key)) { oldValue = segment.put(key, newValue); } else { oldValue = segment.get(key); if (oldValue.isExpired()) { segment.put(key, newValue); } else if (refreshTimeout) { oldValue = new ExpiringObject(oldValue.getValue(), System.currentTimeMillis()); segment.put(key, oldValue); } } } if (oldValue == null || oldValue.isExpired()) { return null; } return oldValue.getValue(); } @SuppressWarnings("unchecked") @Override public V get(final Object key) { final CacheMap<K, ExpiringObject> segment = segment(key); final ExpiringObject oldValue; synchronized (segment) { oldValue = segment.get(key); } if (oldValue != null) { if (expire(segment, (K) key, oldValue)) { return null; } else { LOGGER.debug("Get found. Key: {}. Value: {}.", key, oldValue.getValue()); return oldValue.getValue(); } } LOGGER.debug("Get not found. Key: {}.", key); return null; } @Override public V remove(final Object key) { final CacheMap<K, ExpiringObject> segment = segment(key); final ExpiringObject oldValue; synchronized (segment) { oldValue = segment.remove(key); } if (oldValue == null || oldValue.isExpired()) { return null; } return oldValue.getValue(); } @SuppressWarnings("unchecked") @Override public boolean remove(final Object key, final Object value) { final CacheMap<K, ExpiringObject> segment = segment(key); final ExpiringObject oldValue; boolean removed = false; synchronized (segment) { oldValue = segment.get(key); if (oldValue != null && oldValue.equals(value) && !oldValue.isExpired()) { removed = segment.remove(key) != null; } } if (oldValue != null) { expire(segment, (K) key, oldValue); } return removed; } @SuppressWarnings("unchecked") @Override public boolean containsKey(final Object key) { final CacheMap<K, ExpiringObject> segment = segment(key); final ExpiringObject oldValue; synchronized (segment) { oldValue = segment.get(key); } if (oldValue != null) { if (!expire(segment, (K) key, oldValue)) { return true; } } return false; } @Override public boolean containsValue(final Object value) { for (final CacheMap<K, ExpiringObject> segment : segments) { synchronized (segment) { expireSegment(segment); if (segment.containsValue(value)) { return true; } } } return false; } @Override public int size() { int size = 0; for (final CacheMap<K, ExpiringObject> segment : segments) { synchronized (segment) { expireSegment(segment); size += segment.size(); } } return size; } @Override public boolean isEmpty() { for (final CacheMap<K, ExpiringObject> segment : segments) { synchronized (segment) { expireSegment(segment); if (!segment.isEmpty()) { return false; } } } return true; } @Override public void clear() { for (final CacheMap<K, ExpiringObject> segment : segments) { synchronized (segment) { segment.clear(); } } } @Override public int hashCode() { int hashCode = 0; for (final CacheMap<K, ExpiringObject> segment : segments) { synchronized (segment) { expireSegment(segment); // as seen in AbstractMap hashCode += segment.hashCode(); } } return hashCode; } @Override public Set<K> keySet() { final Set<K> retVal = new HashSet<K>(); for (final CacheMap<K, ExpiringObject> segment : segments) { synchronized (segment) { expireSegment(segment); retVal.addAll(segment.keySet()); } } return retVal; } @Override public void putAll(final Map<? extends K, ? extends V> inMap) { for (final Entry<? extends K, ? extends V> e : inMap.entrySet()) { this.put(e.getKey(), e.getValue()); } } @Override public Collection<V> values() { final Collection<V> retVal = new ArrayList<V>() { private static final long serialVersionUID = 3769009451779243542L; @Override public Iterator<V> iterator() { final Iterator<V> orig = super.iterator(); return new Iterator<V>() { @Override public boolean hasNext() { return orig.hasNext(); } @Override public V next() { return orig.next(); } @Override public void remove() { throw new UnsupportedOperationException("Cannot remove from values."); } }; } }; for (final CacheMap<K, ExpiringObject> segment : segments) { synchronized (segment) { final Iterator<ExpiringObject> iterator = segment.values().iterator(); while (iterator.hasNext()) { final ExpiringObject expiringObject = iterator.next(); if (expiringObject.isExpired()) { iterator.remove(); LOGGER.debug("Remove in entry set: {}.", expiringObject.getValue()); removedCounter.incrementAndGet(); } else { retVal.add(expiringObject.getValue()); } } } } return retVal; } @Override public Set<Map.Entry<K, V>> entrySet() { final Set<Map.Entry<K, V>> retVal = new HashSet<Map.Entry<K, V>>() { private static final long serialVersionUID = 3769009451779243542L; @Override public Iterator<java.util.Map.Entry<K, V>> iterator() { final Iterator<Map.Entry<K,V>> orig = super.iterator(); return new Iterator<Map.Entry<K,V>>() { private K currentKey = null; @Override public boolean hasNext() { return orig.hasNext(); } @Override public java.util.Map.Entry<K, V> next() { java.util.Map.Entry<K, V> entry = orig.next(); currentKey = entry.getKey(); return entry; } @Override public void remove() { orig.remove(); if(currentKey != null) { ConcurrentCacheMap.this.remove(currentKey); } } }; } }; for (final CacheMap<K, ExpiringObject> segment : segments) { synchronized (segment) { final Iterator<Map.Entry<K, ExpiringObject>> iterator = segment.entrySet().iterator(); while (iterator.hasNext()) { final Map.Entry<K, ExpiringObject> entry = iterator.next(); if (entry.getValue().isExpired()) { iterator.remove(); LOGGER.debug("Removed in entry set: {}.", entry.getValue().getValue()); removedCounter.incrementAndGet(); } else { retVal.add(new Map.Entry<K, V>() { @Override public K getKey() { return entry.getKey(); } @Override public V getValue() { return entry.getValue().getValue(); } @Override public V setValue(final V value) { throw new UnsupportedOperationException("not supported"); } }); } } } } return retVal; } @Override public boolean replace(final K key, final V oldValue, final V newValue) { final ExpiringObject oldValue2 = new ExpiringObject(oldValue, 0L); final ExpiringObject newValue2 = new ExpiringObject(newValue, System.currentTimeMillis()); final CacheMap<K, ExpiringObject> segment = segment(key); final ExpiringObject oldValue3; boolean replaced = false; synchronized (segment) { oldValue3 = segment.get(key); if (oldValue3 != null && !oldValue3.isExpired() && oldValue2.getValue().equals(oldValue3.getValue())) { segment.put(key, newValue2); replaced = true; } } if (oldValue3 != null) { expire(segment, key, oldValue3); } return replaced; } @Override public V replace(final K key, final V value) { final ExpiringObject newValue = new ExpiringObject(value, System.currentTimeMillis()); final CacheMap<K, ExpiringObject> segment = segment(key); final ExpiringObject oldValue; synchronized (segment) { oldValue = segment.get(key); if (oldValue != null && !oldValue.isExpired()) { segment.put(key, newValue); } } if (oldValue == null) { return null; } if (expire(segment, key, oldValue)) { return null; } return oldValue.getValue(); } /** * Expires a key in a segment. If a key value pair is expired, it will get removed. * * @param segment * The segment * @param key * The key * @param value * The value * @return True if expired, otherwise false. */ private boolean expire(final CacheMap<K, ExpiringObject> segment, final K key, final ExpiringObject value) { if (value.isExpired()) { synchronized (segment) { final ExpiringObject tmp = segment.get(key); if (tmp != null && tmp.equals(value)) { segment.remove(key); LOGGER.debug("Removed in expire: {}.", value.getValue()); removedCounter.incrementAndGet(); } } return true; } return false; } /** * Fast expiration. Since the ExpiringObject is ordered the for loop can break early if a object is not expired. * * @param segment * The segment */ private void expireSegment(final CacheMap<K, ExpiringObject> segment) { final Iterator<ExpiringObject> iterator = segment.values().iterator(); while (iterator.hasNext()) { final ExpiringObject expiringObject = iterator.next(); if (expiringObject.isExpired()) { iterator.remove(); LOGGER.debug("Remove in expire segment: {}.", expiringObject.getValue()); removedCounter.incrementAndGet(); } else { break; } } } /** * @return The number of expired objects */ public int expiredCounter() { return removedCounter.get(); } /** * An object that also holds expiration information. */ private class ExpiringObject { private final V value; private final long lastAccessTime; /** * Creates a new expiring object with the given time of access. * * @param value * The value that is wrapped in this instance * @param lastAccessTimeMillis * The time of access in milliseconds. */ ExpiringObject(final V value, final long lastAccessTimeMillis) { if (value == null) { throw new IllegalArgumentException("An expiring object cannot be null."); } this.value = value; this.lastAccessTime = lastAccessTimeMillis; } /** * @return If entry is expired */ public boolean isExpired() { return System.currentTimeMillis() >= lastAccessTime + (TimeUnit.MILLISECONDS.convert(timeToLiveSeconds, TimeUnit.SECONDS)); } /** * @return The wrapped value */ public V getValue() { return value; } @Override public boolean equals(final Object obj) { if (!(obj instanceof ConcurrentCacheMap.ExpiringObject)) { return false; } @SuppressWarnings("unchecked") final ExpiringObject exp = (ExpiringObject) obj; return value.equals(exp.value); } @Override public int hashCode() { return value.hashCode(); } } }