/*
* Created on Nov 19, 2007
*/
package ecologylab.generic;
import java.util.ArrayList;
import java.util.HashSet;
/**
* This class provides access to a pool of pre-allocated resources. The pool grows and contracts
* throughout its lifetime to suit the number of resources necessary and to attempt to minimize
* memory footprints; acquire() and release() are amortized O(1) complexity (although
* expansion/contraction triggers may take longer).
*
* The primary way of accessing resources that are controlled by a pool are through the acquire and
* release methods. Every acquire should have a matching release, to ensure that resources may be
* recycled by later calls to acquire.
*
* Subclasses of ResourcePool ensure that the resources obtained through the acquire method are
* "clean", that is, they are immediately ready for use as if they were just instantiated.
*
* @author Zachary O. Toups (zach@ecologylab.net)
*/
public abstract class ResourcePool<T> extends Debug
{
protected static final int DEFAULT_POOL_SIZE = 16;
public static final int NEVER_CONTRACT = -1;
private final ArrayList<T> pool;
/**
* The number of resources this pool is currently in control of, including the number of resources
* that are currently acquired by other processes.
*/
private int capacity;
/**
* Specifies the minimum size for the backing store, to prevent thrashing when small numbers of
* objects are needed.
*/
private final int minCapacity;
private final float loadFactor = .75f;
/**
* If true, stores the hash of each resource that is released to this pool and throws an exception
* if a resource is released more than once.
*/
private boolean checkMultiRelease;
/**
* Stores a listing of the hashes of all of the resources currently held by the resource pool.
* This field is only instantiated if checkMultiRelease is true, if it is false, this field will
* be NULL.
*/
private final HashSet<T> releasedResourceHashes;
/**
* Special constructor that will only instantiate the backing pool resources if the first argument
* is true. This method can be used by subclasses to set up member variables before calling
* instantiateResourcesInPool(), so that the instantiation will use the member variables.
*
* @param instantiateResourcesInPool
* @param initialPoolSize
* @param minimumPoolSize
* the size of the pool will never contract below this value. If NEVER_CONTRACT is
* passed, the pool will never contract.
*/
protected ResourcePool( boolean instantiateResourcesInPool,
int initialPoolSize,
int minimumPoolSize,
boolean checkMultiRelease)
{
this.capacity = Math.max(initialPoolSize, minimumPoolSize);
this.pool = new ArrayList<T>(capacity);
if (checkMultiRelease)
releasedResourceHashes = new HashSet<T>();
else
releasedResourceHashes = null;
if (instantiateResourcesInPool)
instantiateResourcesInPool();
this.minCapacity = minimumPoolSize;
this.checkMultiRelease = checkMultiRelease;
}
/**
* Creates a new ResourcePool with the specified initialPoolSize (or minimumPoolSize,
* minimumPoolSize > initialPoolSize) and minimum capacity.
*
* Note that this constructor will call generateNewResource (capacity) times to fill in the
* backing collection. If generateNewResource relies upon setting fields, the subclass should
* *NOT* call this constructor and should instead call ResourcePool(boolean, int, int).
*
* @param initialPoolSize
* the initial size of the backing pool of objects.
* @param minimumPoolSize
* the minimum size for the backing pool of objects. This is important to specify,
* otherwise repeatedly aquire()'ing and release()'ing resources can negatively affect
* performance.
*/
protected ResourcePool(int initialPoolSize, int minimumPoolSize)
{
this(true, initialPoolSize, minimumPoolSize, false);
}
/**
* Releases all objects owned by this pool.
*/
protected synchronized void shutdown()
{
int size = this.pool.size();
for (int i = size - 1; i >= 0; i--)
{
onRemoval(this.remove(i));
}
}
protected void onAcquire()
{
}
/**
* Take a resource from the pool, making it unavailable for other segments of the program to use.
*
* Objects returned by calls to acquire() are "clean", that is, they are in the same state they
* would be in as if they were just instantiated.
*
* @return
*/
public final T acquire()
{
T retVal;
int freeIndex;
synchronized (this)
{ // when acquire()'ing it might be necessary to expand the size of the
// backing store
onAcquire();
freeIndex = this.pool.size() - 1;
if (freeIndex == -1)
{
this.expandPool();
freeIndex = this.pool.size() - 1;
}
retVal = this.remove(freeIndex);
}
this.clean(retVal);
return retVal;
}
protected void onRelease(T resourceToRelease)
{
}
/**
* Return a resource for use by another part of the program. The resource will be cleaned at some
* later time when it is acquire()'ed. Resources that are released should NOT be used again.
*
* @param resourceToRelease
* @return null; this is meant as a convenience, so that the programmer can use the line: resource
* = recPool.release(resource);, automatically unlinking the released resource from the
* binding in the resource user's code.
*/
public final synchronized T release(T resourceToRelease)
{
if (resourceToRelease != null)
{
synchronized (this)
{
if (!this.add(resourceToRelease))
return null;
int poolSize = pool.size();
if (minCapacity != NEVER_CONTRACT && capacity > minCapacity
&& poolSize > loadFactor * capacity)
{
this.contractPool();
}
onRelease(resourceToRelease);
}
}
else
{
warning("attempt to load a null reference into resource pool.");
}
return null;
}
/**
* Instantiates a resource of type T.
*
* @return
*/
protected abstract T generateNewResource();
/**
* Ensure that the given Object is "clean", that is, in the state it would be in if it were just
* instantiated. For example, if this class were handling StringBuilders, it should ensure that
* the StringBuilder does not contain any characters from a previous use.
*
* clean(T) is automatically called immediately before an object is returned from the acquire()
* method.
*
* @param objectToClean
*/
protected abstract void clean(T objectToClean);
/**
* Increases the number of resources to make available. Doubles the backing Collection size and
* instantiates objects to match.
*
* expandPool() is not thread-safe and should only be called within a synchronized block.
*/
private void expandPool()
{
int oldCap = capacity;
if (capacity > 0)
{
/*
* double capacity by instantiating <capacity> new objects; this doubles, b/c when this method
* is called, we are empty and have already dealt-out <capacity> objects
*/
instantiateResourcesInPool();
capacity *= 2;
}
else
{ // capacity is 0, just put one in there
this.add(this.generateNewResource());
capacity = 1;
}
debug("expanding pool from " + oldCap + " elements to " + capacity + " elements");
this.pool.ensureCapacity(capacity);
}
private void contractPool()
{
int oldCap = capacity;
capacity /= 2;
for (int i = 0; (i < capacity && pool.size() > minCapacity); i++)
onRemoval(this.remove(pool.size() - 1));
if (capacity < minCapacity)
{
capacity = minCapacity;
}
// this is kludge, but it can't be done any other with with ArrayList.
// Both of these cost an arrayCopy(), but the
// alternative is to end up with an arrayCopy on each subsequent add().
pool.trimToSize();
pool.ensureCapacity(capacity);
debug("contracting pool from " + oldCap + " elements to " + capacity + " elements");
}
/**
* Lets a subclass control what happens to the object when it is thrown away, letting the
* developer release any resources they may have allocated.
*
* onRemoval(T) is called when the object is about to removed during a contraction
*
* @param remove
*/
protected void onRemoval(T remove)
{
}
/**
*
*/
protected void instantiateResourcesInPool()
{
for (int i = 0; i < capacity; i++)
this.add(this.generateNewResource());
}
/**
* Removes a resource from the pool to be either returned or recycled. Handles housekeeping with
* the hashes if necessary.
*
* @param index
* @return
*/
private T remove(int index)
{
T removedResource = pool.remove(index);
if (this.checkMultiRelease)
this.releasedResourceHashes.remove(removedResource);
return removedResource;
}
/**
* Adds a resource to the pool. Handles housekeeping with hashes if necessary.
*
* @param resource
*/
private boolean add(T resource)
{
if (this.checkMultiRelease && !this.releasedResourceHashes.add(resource))
throw new RuntimeException("Attempted to release the same resource more than once: "
+ resource.toString());
pool.add(resource);
return true;
}
/**
* @return the capacity
*/
public int getCapacity()
{
return capacity;
}
public int getPoolSize()
{
return this.pool.size();
}
}