RE: 一定時間しか保持しないMap

id:Yoshioriが何か変な事してたんで、考えてみた。

ぱっと思いついた問題点。

  • cleanメソッドがヒドイ。
    • あらゆるMapオペレーションの度に、値を全走査するとか、無いんじゃね?
  • TimeAndValueがヒドイ。
    • Dateじゃなくて、long持てば良くね?
  • 非同期処理中に使うとハマるんじゃね?
    • 何にこの実装を使うのか知らんけど、キャッシュとかじゃねぇの?
  • TimeoutMap
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class TimeoutMap<K, V> extends AbstractMap<K, V> implements
    ConcurrentMap<K, V>, Serializable {

  private static final long serialVersionUID = 9162835424791321760L;

  protected ScheduledExecutorService cleaner = Executors
      .newScheduledThreadPool(1);

  protected ConcurrentMap<K, V> delegate;

  protected final long timeout;

  public TimeoutMap(long timeout) {
    this.delegate = new ConcurrentHashMap<K, V>();
    this.timeout = timeout;
  }

  public TimeoutMap(long timeout, int initialCapacity) {
    this.delegate = new ConcurrentHashMap<K, V>(initialCapacity);
    this.timeout = timeout;
  }

  public void dispose() {
    cleaner.shutdown();
  }

  public void clear() {
    delegate.clear();
  }

  public boolean containsKey(Object key) {
    return delegate.containsKey(key);
  }

  public boolean containsValue(Object value) {
    return delegate.containsValue(value);
  }

  public Set<java.util.Map.Entry<K, V>> entrySet() {
    return delegate.entrySet();
  }

  public boolean equals(Object o) {
    return delegate.equals(o);
  }

  public V get(Object key) {
    return this.delegate.get(key);
  }

  public int hashCode() {
    return this.delegate.hashCode();
  }

  public boolean isEmpty() {
    return this.delegate.isEmpty();
  }

  public Set<K> keySet() {
    return this.delegate.keySet();
  }

  public V put(final K key, V value) {
    V result = delegate.put(key, value);
    scheduleRemove(key);
    return result;
  }

  protected void scheduleRemove(final K key) {
    cleaner.schedule(new Runnable() {
      @Override
      public void run() {
        delegate.remove(key);
      }
    }, timeout, TimeUnit.MILLISECONDS);
  }

  public void putAll(Map<? extends K, ? extends V> m) {
    for (K key : m.keySet()) {
      scheduleRemove(key);
    }
    delegate.putAll(m);
  }

  public V putIfAbsent(final K key, V value) {
    V result = delegate.putIfAbsent(key, value);
    scheduleRemove(key);
    return result;
  }

  public boolean remove(Object key, Object value) {
    return delegate.remove(key, value);
  }

  public V remove(Object key) {
    return delegate.remove(key);
  }

  public boolean replace(K key, V oldValue, V newValue) {
    scheduleRemove(key);
    return delegate.replace(key, oldValue, newValue);
  }

  public V replace(K key, V value) {
    scheduleRemove(key);
    return delegate.replace(key, value);
  }

  public int size() {
    return delegate.size();
  }

  public Collection<V> values() {
    return delegate.values();
  }

}
  • 超テキトーなテストコード
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

import org.junit.Test;

public class TimeoutMapTest {

  @Test
  public void testPutKV() throws Exception {
    TimeoutMap<String, String> map = new TimeoutMap<String, String>(1000);
    try {
      map.put("aaa", "1111");
      map.put("bbb", "2222");
      map.put("ccc", "3333");
      map.put("ddd", "4444");

      for (String v : map.values()) {
        System.out.println(v);
      }

      Thread.sleep(1000);

      assertEquals(0, map.size());
      assertNull(map.get("aaa"));

    } finally {
      map.dispose();
    }
  }

}

ぱっと思いついたまんま実装したので、id:Yoshioriの実装で抱えている問題点は全部解決している。
但し、全然違う問題を抱え込む事になっている。

  • メモリの使い方が超贅沢
    • cleaner用に新しいThreadが立ちます。
    • ConcurrentHashMapは、超贅沢なMapです。
      マルチスレッド環境において、そのコストに正当性は勿論あるのだけど、もし、シングルスレッド環境で使うなら、そこに妥当性は存在しませぬ。
  • timeout時の自動削除は、規定の時間を越えても動作しない可能性があります。
    • cleanerスレッドは1つしか立ててないし、そのスレッド数を外からカスタマイズ出来ないので、
      物凄い量のエントリを突っ込むと多分きっと間違いなくtimeoutしてる筈なのに、消えないエントリが出てくるます。
  • TimeoutMapを使い終わったら、disposeメソッドを呼ばないと、cleanerスレッドが残ったままになるます。つまりメモリリークの危険がある。


こんな感じでどうか?

追記::

おかしな部分があったので修正。