ObjectLock.java

/*
 * ObjectLock
 *
 * $Id$
 * $URL$
 */
package gov.usgs.util;

import java.util.HashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Reentrant ReadWrite Locking per object.
 *
 * This is intended for use when multiple sections of code should allow
 * concurrent access, but only when operating on independent objects.
 *
 * @param <T>
 *            The type of object used for locking. This object is used as a key
 *            in a HashMap. Objects that are equal, but not necessarily ==,
 *            reference the same lock.
 */
public class ObjectLock<T> {

	/** map object to corresponding lock for object. */
	private HashMap<T, ReentrantReadWriteLock> locks = new HashMap<T, ReentrantReadWriteLock>();

	/**
	 * keep track of how many threads have, or are about to, use the lock object
	 * in the map 'locks'. When this count reaches zero, it is safe to remove
	 * the lock object.
	 */
	private HashMap<T, Integer> lockThreadCounts = new HashMap<T, Integer>();

	private final Object syncObject = new Object();

	/**
	 * Construct a new ObjectLock object.
	 */
	public ObjectLock() {
	}

	/**
	 * Get the lock for an object.
	 *
	 * This method must only be called by one thread at a time. synchronization
	 * is handled by other methods in this class.
	 *
	 * @param object
	 *            object to lock.
	 * @return lock corresponding to object.
	 */
	private ReentrantReadWriteLock getLock(final T object) {
		ReentrantReadWriteLock lock = locks.get(object);
		if (lock == null) {
			lock = new ReentrantReadWriteLock(true);
			locks.put(object, lock);
		}

		return lock;
	}

	/**
	 * Increment the thread count for an object.
	 *
	 * This method must only be called by one thread at a time. synchronization
	 * is handled by other methods in this class.
	 *
	 * @param object
	 */
	private void incrementThreadCount(final T object) {
		Integer threadCount = lockThreadCounts.get(object);
		if (threadCount == null) {
			threadCount = Integer.valueOf(0);
		}
		threadCount = threadCount + 1;
		lockThreadCounts.put(object, threadCount);
	}

	/**
	 * Decrement the thread count for an object. Also, when the thread count
	 * reaches zero, the lock corresponding to this object is removed from the
	 * locks map.
	 *
	 * This method must only be called by one thread at a time. synchronization
	 * is handled by other methods in this class.
	 *
	 * @param object
	 */
	private void decrementThreadCount(final T object) {
		Integer threadCount = lockThreadCounts.get(object);
		if (threadCount == null) {
			throw new IllegalStateException(
					"Trying to decrement thread count that does not exist.");
		}
		threadCount = threadCount - 1;
		lockThreadCounts.put(object, threadCount);

		if (threadCount == 0) {
			// no threads are using this lock anymore, cleanup
			locks.remove(object);
			lockThreadCounts.remove(object);
		}
	}

	/**
	 * Acquire a read lock for an object.
	 *
	 * Callers MUST subsequently call releaseReadLock.
	 *
	 * @param object
	 *            the object to lock for reading.
	 * @throws InterruptedException if thread is interrupted
	 */
	public void acquireReadLock(final T object) throws InterruptedException {
		ReentrantReadWriteLock lock = null;
		synchronized (syncObject) {
			lock = getLock(object);
			incrementThreadCount(object);
		}
		// do this outside synchronized for concurrency
		lock.readLock().lockInterruptibly();
	}

	/**
	 * Check if the calling thread currently has a write lock for the object.
	 *
	 * @param object
	 *            object to check.
	 * @return true if the current thread currently holds a write lock, false
	 *         otherwise.
	 */
	public boolean haveWriteLock(final T object) {
		ReentrantReadWriteLock lock = locks.get(object);
		if (lock == null) {
			return false;
		}
		return lock.isWriteLockedByCurrentThread();
	}

	/**
	 * Release a held read lock for an object.
	 *
	 * Callers MUST have previously called acquireReadLock(object).
	 *
	 * @param object
	 *            the object to unlock for reading.
	 */
	public void releaseReadLock(final T object) {
		synchronized (syncObject) {
			ReentrantReadWriteLock lock = getLock(object);
			decrementThreadCount(object);
			lock.readLock().unlock();
		}
	}

	/**
	 * Acquire a write lock for an object.
	 *
	 * Callers MUST also call releaseWriteLock(object).
	 *
	 * @param object
	 *            the object to lock for writing.
	 * @throws InterruptedException if thread is interrupted
	 */
	public void acquireWriteLock(final T object) throws InterruptedException {
		ReentrantReadWriteLock lock = null;
		synchronized (syncObject) {
			lock = getLock(object);
			incrementThreadCount(object);
		}
		// do this outside synchronized for concurrency
		lock.writeLock().lockInterruptibly();
	}

	/**
	 * Release a held write lock for an object.
	 *
	 * Callers MUST have previously called acquireWriteLock(object).
	 *
	 * @param object
	 *            the object to unlock for writing.
	 */
	public void releaseWriteLock(final T object) {
		synchronized (syncObject) {
			ReentrantReadWriteLock lock = getLock(object);
			decrementThreadCount(object);
			lock.writeLock().unlock();
		}
	}

	/**
	 * This is a synonym for acquireWriteLock, which is an exclusive lock for
	 * this object.
	 *
	 * @param object
	 *            the object to lock.
	 * @throws InterruptedException if thread is interrupted
	 */
	public void acquireLock(final T object) throws InterruptedException {
		acquireWriteLock(object);
	}

	/**
	 * This is a synonym for releaseWriteLock, which is an exclusive lock for
	 * this object.
	 *
	 * @param object
	 *            the object to unlock.
	 */
	public void releaseLock(final T object) {
		releaseWriteLock(object);
	}

}