/* license-start
*
* Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, at <http://www.gnu.org/licenses/>.
*
* Contributors:
* Crispico - Initial API and implementation
*
* license-end
*/
package org.flowerplatform.communication.stateful_service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* Locking mechanism that uses <code>Object</code> objects as locks. The difference between this
* and the standard lock mechanism in Java is that <code>.equals()</code> instead of <code>==</code>.
* More precisely:
*
* <pre>
* synchronized (new String("myString")) {
* // some code
* }
* </pre>
*
* In the above case, the block won't give the expected results. 2 threads will still be able to
* execute the block at the same time, because the locks are <strong>different</strong> object instances.
*
* With this class, we can achieve the desired behavior.
* First, instantiate it, probably like an attribute of the class.
* <pre>
* private final NamedLockPool namedLockPool = new NamedLockPool();
* </pre>
*
* Then use it:
* <pre>
* namedLockPool.lock(new String("myString"));
* try {
* // some code
* } finally {
* namedLockPool.unlock(new String("myString"));
* }
* </pre>
*
* Note:
* It is recommended to use <code>String</code> objects as locks.
* Otherwise make sure your objects are unique.
* For the moment, only <code>GenericTreeStatefulService</code> uses Object.
* @author Cristina
*
* @author Cristi
*/
public class NamedLockPool {
static private class LockWithCounter extends ReentrantLock {
private static final long serialVersionUID = 1L;
/**
* @see The comment in the code, below, for details.
*/
private AtomicInteger numberOfThreadsWaitingToLock = new AtomicInteger();
}
private Map<Object, LockWithCounter> currentLocks = new HashMap<Object, LockWithCounter>();
/*
* == Synchronization scenarios ==
* Other thread calls:
* A) lock(_same_key_)
* B) lock(_other_key_)
*
* C) unlock(_same_key_)
* D) unlock(_other_key_)
*
* For each case, the other thread is a little bit BEHIND me. The other way (i.e. the other thread
* is AHEAD OF ME) is documented in the other method.
*/
public void lock(Object key) {
LockWithCounter lockForCurrentKey = null;
// the following block can be executed at the same time, once per instance of this
// class, no matter the key. So in all cases, the threads will wait here.
synchronized (this) {
lockForCurrentKey = currentLocks.get(key);
if (lockForCurrentKey == null) {
lockForCurrentKey = new LockWithCounter();
currentLocks.put(key, lockForCurrentKey);
}
lockForCurrentKey.numberOfThreadsWaitingToLock.incrementAndGet();
}
/*
* == Explanation ==
* This section, until calling .lock() is tricky. While I'm in this section,
* I don't want the unlock() method to remove the lock from the map. That's why we introduced
* numberOfThreadsWaitingToLock: a counter of threads that are in this zone (i.e. I'm preparing to acquire the lock).
*
* The synchronized block doesn't cover all the method (like in the case of unlock()), because we want the waiting (to acquire
* the lock) to happen outside the synchronized block. Otherwise, we would create a big bottle neck, allowing only 1 thread to execute
* at a given moment (regardless of the key).
*
* == Scenarios ==
* The following block can be executed simultaneously.
* For B) and D), everything OK, because the variable points towards different instances.
* For A) the other thread will go to sleep when calling .lock().
* for C) there are 2 cases
* 1) this thread will block when calling .lock(), so the other thread will finish execution, but won't
* remove this lock, because numberOfThreadsWaitingToLock >= 1 while this thread waits for .lock()
* 2) this thread will acquire successfully the lock. While this thread is executing methods after
* the .lock() line, other threads may not call .unlock(); only this thread may call .unsubscribe().
* However, if they try, they'll get an exception, because they try to unlock a lock that's not owned
* by them.
*/
try {
// this call shouldn't throw an exception AFAIK; but to be sure, it's
// wrapped around a try/finally
lockForCurrentKey.lock(); // this call is blocking, until lock is acquired
} finally {
// because this doesn't happen under a synchronized block, we need to ensure that the
// decrementation is done atomically. Otherwise what could happen (although quite rarely):
// 2 threads are incrementing 5, and the result could be 6 (because of the caching of the
// current value)
lockForCurrentKey.numberOfThreadsWaitingToLock.decrementAndGet();
}
}
public void unlock(Object key) {
/*
* C) and D) other thread will wait for this whole method to end. Same for A),
* if the execution pointer of the other thread is before its synchronized block
* A) if the execution pointer is after the synchronized block (e.g. the other thread got suspended/delayed
* right after the synchronized block, resulting in this thread to be AHEAD of the other thread), see
* below:
*/
synchronized (this) {
LockWithCounter lockForCurrentKey = null;
lockForCurrentKey = currentLocks.get(key);
if (lockForCurrentKey == null) {
throw new IllegalArgumentException(String.format("Attempt to unlock the lock with key = %s, by thread = %s; but there is no lock for this key!", key, Thread.currentThread()));
}
if (!lockForCurrentKey.isHeldByCurrentThread()) {
// sanity check; anyway, without this here, the unlock() method would have thrown an exception as well; but at least,
// this is gives a bit more information
throw new IllegalStateException(String.format("Attempt to unlock the lock with key = %s, by thread = %s; but this thread is not the owner of the lock!", key, Thread.currentThread()));
}
// case A) until here, the other thread will wait, at it's .lock() line
lockForCurrentKey.unlock();
/*
* case A) the other thread will continue. If it is the only one waiting,
* numberOfThreadsWaitingToLock will drop to 0, but I won't remove the lock, because the lock
* will be held by other thread. If the other thread quickly finishes and calls .unlock(), we
* have the C) case, discussed at the beginning of this doc
*/
if (lockForCurrentKey.numberOfThreadsWaitingToLock.get() == 0 && !lockForCurrentKey.isLocked()) {
currentLocks.remove(key);
}
}
}
/**
* To facilitate JMX exposure, in order to check the state.
*/
public int getNumberOfRegisteredLocks() {
return currentLocks.size();
}
// static public void main(String[] args) {
// final NamedLockPool pool = new NamedLockPool();
// // 2 separate implementations (instead of only one) to facilitate testing with the debugger
// Thread thread1 = new Thread("User 1") {
// @Override
// public void run() {
// System.out.println(String.format("%s: Outside sync block; preparing to enter", getName()));
// pool.lock(new String("key"));
// try {
// System.out.println(String.format("%s: Inside sync block; instruction 1", getName()));
// System.out.println(String.format("%s: Inside sync block; instruction 2", getName()));
// System.out.println(String.format("%s: Inside sync block; instruction 3", getName()));
// } finally {
// pool.unlock(new String("key"));
// }
// System.out.println(String.format("%s: Outside sync block; after exit", getName()));
// }
// };
//
// Thread thread2 = new Thread("User 2") {
// @Override
// public void run() {
// System.out.println(String.format("%s: Outside sync block; preparing to enter", getName()));
// pool.lock(new String("key"));
// try {
// System.out.println(String.format("%s: Inside sync block; instruction 1", getName()));
// System.out.println(String.format("%s: Inside sync block; instruction 2", getName()));
// System.out.println(String.format("%s: Inside sync block; instruction 3", getName()));
// } finally {
// pool.unlock(new String("key"));
// }
// System.out.println(String.format("%s: Outside sync block; after exit", getName()));
// }
// };
//
// thread1.start();
// thread2.start();
// }
}