com.tectonica.collections.AutoEvictMap Maven / Gradle / Ivy
Show all versions of tectonica-commons Show documentation
/*
* Copyright (C) 2014 Zach Melamed
*
* Latest version available online at https://github.com/zach-m/tectonica-commons
*
* 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 com.tectonica.collections;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* Concurrent, thread-safe container, retaining a key-value map with a reference count mechanism. Instead of the standard {@code get},
* {@code put} and {@code remove} methods, if offers the following:
*
* - {@code acquire} - gets a value given a key, creating it if necessary. If the key already exists, its reference count is increased
*
- {@code release} - decreases a reference count of a value, removing it if the number of acquires matches the number of releases
*
*
* of course, both actions are performed atomically.
*
* When the class is constructed a default {@link Factory} may be provided, which will generate the values when {@link #acquire(Object)}
* needs them. Alternatively, on each invocation of {@link #acquire(Object, Factory)}, a custom ad-hoc factory may be provided for the
* particular key acquired. When keys are released, no additional action is taken.
*
* @author Zach Melamed
*/
public class AutoEvictMap
{
public static interface Factory
{
V valueOf(K key);
}
private final ConcurrentMap> map = new ConcurrentHashMap>();
private final Factory defaultFactory;
public AutoEvictMap()
{
this.defaultFactory = null;
}
public AutoEvictMap(Factory defaultFactory)
{
this.defaultFactory = defaultFactory;
}
public V acquire(final K key) throws InterruptedException
{
if (defaultFactory == null)
throw new NullPointerException("defaultFactory");
return acquire(key, defaultFactory);
}
public V acquire(final K key, final Factory customFactory) throws InterruptedException
{
if (key == null)
throw new NullPointerException("key");
if (customFactory == null)
throw new NullPointerException("customFactory");
Holder holder;
while (true)
{
holder = map.get(key);
if (holder == null)
{
if (map.putIfAbsent(key, holder = new Holder(key, customFactory)) == null)
{
// initial creation of the value
holder.run();
break;
}
}
else
{
if (map.replace(key, holder, holder = holder.inc()))
break; // ref-count increased
}
}
return holder.get(); // NOTE: think whether to remove from map in case of exception/cancellation
}
public boolean release(K key)
{
if (key == null)
throw new NullPointerException("key");
while (true)
{
Holder holder = map.get(key);
if (holder == null)
return true; // was already removed
if (holder.isInitial())
{
if (map.remove(key, holder.initial()))
return true; // removed now
}
else
{
if (map.replace(key, holder, holder.dec()))
return false; // not removed, just decreased ref-count
}
}
}
public int size()
{
return map.size();
}
public void clear()
{
map.clear();
}
// /////////////////////////////////////////////////////////////////////////////////////////
private static class Holder
{
private final FutureTask ft;
private final int refCount;
public Holder(final K key, final Factory generator)
{
ft = new FutureTask(new Callable()
{
public V call() throws InterruptedException
{
return generator.valueOf(key);
}
});
refCount = 1;
}
private Holder(FutureTask ft, int refCount)
{
this.ft = ft;
this.refCount = refCount;
}
public V get() throws InterruptedException
{
try
{
return ft.get();
}
catch (ExecutionException e)
{
Throwable t = e.getCause();
if (t instanceof RuntimeException)
throw (RuntimeException) t;
else if (t instanceof Error)
throw (Error) t;
else
throw new IllegalStateException("Not unchecked", t);
}
}
public void run()
{
ft.run();
}
public Holder inc()
{
return new Holder(ft, refCount + 1);
}
public Holder dec()
{
return new Holder(ft, refCount - 1);
}
public Holder initial()
{
return new Holder(ft, 1);
}
public boolean isInitial()
{
return refCount == 1;
}
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object obj)
{
return (refCount == ((Holder) obj).refCount);
}
}
}