mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-17 01:31:01 +00:00
422 lines
22 KiB
Java
422 lines
22 KiB
Java
package com.sap.sailing.util.impl;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.locks.Lock;
|
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import com.sap.sailing.domain.common.TimePoint;
|
|
import com.sap.sailing.domain.common.impl.MillisecondsTimePoint;
|
|
import com.sap.sailing.domain.common.impl.Util;
|
|
|
|
/**
|
|
* Supports lock management for {@link NamedReentrantReadWriteLock} which is a specialization of
|
|
* {@link ReentrantReadWriteLock} that provides enhanced tracing capabilities. This class offers a number of utility
|
|
* methods which also support the propagation of locks from one thread to another. This can be used by threads to
|
|
* testify that they will wait for another thread which therefore doesn't need to acquire the same locks again. This can
|
|
* help to avoid deadlocks.
|
|
* <p>
|
|
*
|
|
* Locks are propagated from one thread to another in the form of counters managed in the {@link #propagationCounts} map that
|
|
* count, how many other threads have propagated their lock held. When a thread needs to obtain a lock, it first checks
|
|
* if this lock has a positive count in the map. If so, the lock doesn't need to actually be locked because some other
|
|
* thread has asserted that it holds the lock already. Instead, a counter is incremented in the {@link #virtualLockCounts}
|
|
* map that is used to monitor that the "virtual" locks are handled properly and that all of them have been unlocked
|
|
* when unpropagation happens.
|
|
* <p>
|
|
*
|
|
* Unlocking is a bit trickier for read locks because even if a thread A holds the read lock, a thread B may still
|
|
* obtain it. If A then propagates its locks to B, B needs to understand whether it last "obtained" the lock by just
|
|
* incrementing the counter on the propagated lock, or by actually obtaining the lock itself, and then perform the
|
|
* inverse operation. Note that lock propagation and unpropagation may happen more or less at any point in time
|
|
* (synchronized on the receiving thread's {@link #propagationCounts} entry), so thread B may first actually obtain the read
|
|
* lock, then receive propagated locks from thread A, then reentrantly lock again by incrementing the counter. The only
|
|
* guarantee that users of the {@link LockUtil} class need to provide is that the locks are unpropagated only after
|
|
* thread B is done with its locking operations as long as A waits for B. This in particular means that B is required to
|
|
* unlock all locks it obtained (actually or virtually by incrementing the counter) before unpropagation happens. Note
|
|
* also that more than one thread (A_1, A_2, ...) may propagate its locks to the same other thread B.
|
|
*
|
|
* @author Axel Uhl (d043530)
|
|
*
|
|
*/
|
|
public class LockUtil {
|
|
private enum ReadOrWrite { READ, WRITE };
|
|
|
|
private static final int NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK = 5;
|
|
private static final Logger logger = Logger.getLogger(Util.class.getName());
|
|
private static final Map<NamedReentrantReadWriteLock, TimePoint> lastTimeWriteLockWasObtained = new ConcurrentWeakHashMap<NamedReentrantReadWriteLock, TimePoint>();
|
|
|
|
/**
|
|
* Tells how many other threads propagated which held lock to the key thread. During propagation, a lock is
|
|
* considered held if it is really locked by the propagating thread or if the propagating thread received it itself
|
|
* through propagation. The value maps only contain positive (non-zero) values. If a count goes to zero, the
|
|
* corresponding key lock is removed from the map.
|
|
* <p>
|
|
*
|
|
* The thread-specific value maps are used as monitor objects whenever decisions about thread-specific lock counts
|
|
* need to be made.
|
|
*/
|
|
private static final Map<Thread, Map<Lock, Integer>> propagationCounts = new ConcurrentWeakHashMap<Thread, Map<Lock,Integer>>();
|
|
|
|
/**
|
|
* Counts the "virtual" locks. A "virtual" lock is obtained if and only if at the time of calling
|
|
* {@link #lockForRead(NamedReentrantReadWriteLock)} or {@link #lockForWrite(NamedReentrantReadWriteLock)} the
|
|
* respective lock has a positive {@link #propagationCounts propagation count} for the current thread. Entries
|
|
* always have a positive integer value. {@link #decrement(Lock, Map)} removes entries whose integer count would
|
|
* go to 0.
|
|
*/
|
|
private static final Map<Thread, Map<Lock, Integer>> virtualLockCounts = new ConcurrentWeakHashMap<Thread, Map<Lock, Integer>>();
|
|
|
|
/**
|
|
* Redundant but easily accessible hold count per thread and lock. These are the actual lock hold counts as they are
|
|
* recorded in the actual {@link NamedReentrantReadWriteLock} locks.
|
|
*/
|
|
private static final Map<Thread, Map<Lock, Integer>> lockCounts = new ConcurrentWeakHashMap<Thread, Map<Lock, Integer>>();
|
|
|
|
public static void lockForRead(NamedReentrantReadWriteLock lock) {
|
|
acquireLockVirtuallyOrActually(lock, lock.readLock(), ReadOrWrite.READ);
|
|
}
|
|
|
|
public static void lockForWrite(NamedReentrantReadWriteLock lock) {
|
|
acquireLockVirtuallyOrActually(lock, lock.writeLock(), ReadOrWrite.WRITE);
|
|
lastTimeWriteLockWasObtained.put(lock, MillisecondsTimePoint.now());
|
|
}
|
|
|
|
private static void acquireLockVirtuallyOrActually(NamedReentrantReadWriteLock lock, final Lock readOrWriteLock, final ReadOrWrite readOrWrite) {
|
|
boolean locked = false;
|
|
while (!locked) {
|
|
final Map<Lock, Integer> currentThreadsPropagationCounts = getCurrentThreadsPropagationCounts();
|
|
synchronized (currentThreadsPropagationCounts) {
|
|
if (currentThreadsPropagationCounts.containsKey(readOrWriteLock)) {
|
|
// read lock was propagated to current thread by at least one other thread; use virtual lock by incrementing counter:
|
|
incrementVirtualLockCountForCurrentThread(readOrWriteLock);
|
|
// Uncomment the following in case of issues you want to debug with logging:
|
|
// logger.finest("only incremented virtual count for "+readOrWrite+" lock " + lock.getName() + " in thread "
|
|
// + Thread.currentThread().getName()+" to "+getCurrentThreadsVirtualLockCounts().get(readOrWriteLock));
|
|
locked = true;
|
|
} else {
|
|
// lock was not yet propagated; try to actually obtain lock
|
|
locked = lock(readOrWriteLock, readOrWrite==ReadOrWrite.READ ? lock.getReadLockName() : lock.getWriteLockName(), lock);
|
|
if (locked) {
|
|
// Uncomment the following in case of issues you want to debug with logging:
|
|
// logger.finest("actually acquired "+readOrWrite+" lock " + lock.getName() + " in thread " + Thread.currentThread().getName());
|
|
incrementLockCountForCurrentThread(readOrWriteLock);
|
|
}
|
|
}
|
|
}
|
|
if (!locked) {
|
|
Thread.yield(); // let any other thread that is trying to propagate locks get access to currentThreadsPropagationCounts
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void unlockAfterRead(NamedReentrantReadWriteLock lock) {
|
|
final ReadLock readOrWriteLock = lock.readLock();
|
|
unlockVirtuallyOrActually(lock, readOrWriteLock);
|
|
}
|
|
|
|
private static void unlockVirtuallyOrActually(NamedReentrantReadWriteLock lock, final Lock readOrWriteLock) {
|
|
Map<Lock, Integer> currentThreadPropagationCounts = getCurrentThreadsPropagationCounts();
|
|
synchronized (currentThreadPropagationCounts) {
|
|
assert isInCurrentThreadsLockSet(readOrWriteLock);
|
|
Integer virtualLockCount = getCurrentThreadsVirtualLockCounts().get(readOrWriteLock);
|
|
if (virtualLockCount != null) {
|
|
// an entry is a positive entry and means the current thread acquired the lock virtually; unlock virtually again
|
|
decrementVirtualLockCountForCurrentThread(readOrWriteLock);
|
|
// Uncomment the following in case of issues you want to debug with logging:
|
|
// logger.finest("only decremented virtual count for "+readOrWrite+" lock "+lock.getName()+" in thread "+Thread.currentThread().getName()+" from "+virtualLockCount);
|
|
} else {
|
|
// Uncomment the following in case of issues you want to debug with logging:
|
|
// logger.finest("actually unlocking "+readOrWrite+" lock "+lock.getName()+" in thread "+Thread.currentThread().getName());
|
|
readOrWriteLock.unlock();
|
|
decrementLockCountForCurrentThread(readOrWriteLock);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void unlockAfterWrite(NamedReentrantReadWriteLock lock) {
|
|
unlockVirtuallyOrActually(lock, lock.writeLock());
|
|
final TimePoint timePointWriteLockWasObtained;
|
|
timePointWriteLockWasObtained = lastTimeWriteLockWasObtained.get(lock);
|
|
if (timePointWriteLockWasObtained == null) {
|
|
logger.info("Internal error: write lock " + lock.getName()
|
|
+ " to be unlocked but no time recorded for when it was last obtained.\n"
|
|
+ "This is where the lock interaction happened:\n" + getCurrentStackTrace());
|
|
} else {
|
|
TimePoint now = MillisecondsTimePoint.now();
|
|
final long heldWriteLockForMillis = now.asMillis() - timePointWriteLockWasObtained.asMillis();
|
|
if (heldWriteLockForMillis > 10000l) {
|
|
String stackTrace = getCurrentStackTrace();
|
|
logger.info("write lock " + lock.getName() + " was held for more than 10s (" + heldWriteLockForMillis
|
|
+ "ms). It got unlocked here: " + stackTrace);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ATTENTION: Calling this method makes a very strong assertion! It asserts that the calling thread will call
|
|
* {@link #unpropagateLockSetFrom(Thread)} for the same <code>from</code> thread passed to this call before
|
|
* <code>from</code> releases any of the locks it currently holds. The effect of making this call is that the
|
|
* calling thread, when trying to acquire a lock already held by <code>from</code>, will not actually acquire that
|
|
* lock again. This, in particular, has the effect that a read lock already held by <code>from</code> will not
|
|
* have to be acquired again, which in turn avoids a read-read deadlock on a <em>fair</em> lock in case another
|
|
* thread is attempting to acquire the corresponding write lock before the current thread tries to re-acquire the
|
|
* read lock.
|
|
* <p>
|
|
*
|
|
* Always use this in a <code>try/finally</code> combination where in the <code>finally</code> block you call
|
|
* {@link #unpropagateLockSetFrom(Thread)}.
|
|
*/
|
|
public static void propagateLockSetFrom(Thread from) {
|
|
Thread to = Thread.currentThread();
|
|
propagateLockSet(from, to);
|
|
}
|
|
|
|
/**
|
|
* Propagates all locks that <code>from</code> received itself through propagation and those that <code>from</code> actually
|
|
* locked itself. If <code>from</code> received a lock through propagation <em>and</em> actually holds it, it is only
|
|
* propagated once.
|
|
*/
|
|
private static void propagateLockSet(Thread from, Thread to) {
|
|
Set<Lock> locksToPropagate = getLocksHeldVirtuallyOrActuallyBy(from);
|
|
Map<Lock, Integer> toMap = getPropagationCounts(to);
|
|
synchronized (toMap) {
|
|
for (Lock lockToPropagate : locksToPropagate) {
|
|
increment(lockToPropagate, toMap);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ATTENTION: Calling this method makes a very strong assertion! It asserts that the calling thread will not call
|
|
* {@link #unpropagateLockSetTo(Thread)} for the same <code>to</code> thread passed to this call before the calling
|
|
* thread releases any of the locks it currently holds. The effect of making this call is that the <code>to</code>
|
|
* thread, when trying to acquire a lock already held by the calling thread, will not actually acquire that lock
|
|
* again. This, in particular, has the effect that a read lock already held by the calling thread will not have to
|
|
* be acquired again by <code>to</code>, which in turn avoids a read-read deadlock on a <em>fair</em> lock in case
|
|
* another thread is attempting to acquire the corresponding write lock before <code>to</code> tries to re-acquire
|
|
* the read lock.
|
|
* <p>
|
|
*
|
|
* Always use this in a <code>try/finally</code> combination where in the <code>finally</code> block you call
|
|
* {@link #unpropagateLockSetTo(Thread)}.
|
|
*/
|
|
public static void propagateLockSetTo(Thread to) {
|
|
Thread from = Thread.currentThread();
|
|
propagateLockSet(from, to);
|
|
}
|
|
|
|
/**
|
|
* A thread that previously propagated the lock set from another thread by using {@link #propagateLockSetFrom(Thread)}
|
|
* ends the propagation with this call. After this call, <code>thread</code> is free to release any locks it held
|
|
* at the time {@link #propagateLockSetFrom(Thread)} was called by the current thread with <code>thread</code> as
|
|
* the argument.
|
|
*/
|
|
public static void unpropagateLockSetFrom(Thread from) {
|
|
Thread to = Thread.currentThread();
|
|
unpropagateLockSet(from, to);
|
|
}
|
|
|
|
private static void unpropagateLockSet(Thread from, Thread to) {
|
|
Set<Lock> locksToPropagate = getLocksHeldVirtuallyOrActuallyBy(from);
|
|
Map<Lock, Integer> toMap = getPropagationCounts(to);
|
|
synchronized (toMap) {
|
|
for (Lock lockToPropagate : locksToPropagate) {
|
|
decrement(lockToPropagate, toMap);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Set<Lock> getLocksHeldVirtuallyOrActuallyBy(Thread thread) {
|
|
Set<Lock> locksToPropagate = new HashSet<Lock>();
|
|
Map<Lock, Integer> propagationMap = propagationCounts.get(thread);
|
|
if (propagationMap != null) {
|
|
// first synchronize fromMap, then toMap; this way, no deadlock can occur as long as propagation works in the same direction
|
|
synchronized (propagationMap) {
|
|
for (Map.Entry<Lock, Integer> otherEntry : propagationMap.entrySet()) {
|
|
locksToPropagate.add(otherEntry.getKey());
|
|
}
|
|
}
|
|
}
|
|
Map<Lock, Integer> lockMap = lockCounts.get(thread);
|
|
if (lockMap != null) {
|
|
for (Map.Entry<Lock, Integer> otherEntry : lockMap.entrySet()) {
|
|
locksToPropagate.add(otherEntry.getKey());
|
|
}
|
|
}
|
|
return locksToPropagate;
|
|
}
|
|
|
|
/**
|
|
* A thread that previously propagated the lock set to another thread by using {@link #propagateLockSetTo(Thread)}
|
|
* ends the propagation with this call. After this call, the current thread is free to release any locks it held
|
|
* at the time {@link #propagateLockSetTo(Thread)} was called by the current thread with <code>to</code> as
|
|
* the argument.
|
|
*/
|
|
public static void unpropagateLockSetTo(Thread to) {
|
|
Thread from = Thread.currentThread();
|
|
unpropagateLockSet(from, to);
|
|
}
|
|
|
|
private static Map<Lock, Integer> getCurrentThreadsPropagationCounts() {
|
|
final Thread currentThread = Thread.currentThread();
|
|
return getPropagationCounts(currentThread);
|
|
}
|
|
|
|
private static Map<Lock, Integer> getCurrentThreadsLockCounts() {
|
|
final Thread currentThread = Thread.currentThread();
|
|
return getLockCounts(currentThread);
|
|
}
|
|
|
|
private static Map<Lock, Integer> getCurrentThreadsVirtualLockCounts() {
|
|
final Thread currentThread = Thread.currentThread();
|
|
return getVirtualLockCounts(currentThread);
|
|
}
|
|
|
|
private static Map<Lock, Integer> getOrCreateMapForThread(final Thread thread, Map<Thread, Map<Lock, Integer>> map) {
|
|
// don't synchronize all the frequent read accesses
|
|
Map<Lock, Integer> result = map.get(thread);
|
|
if (result == null) {
|
|
// but if we need to create a new entry, ensure that this doesn't happen concurrently
|
|
synchronized (map) {
|
|
result = map.get(thread);
|
|
if (result == null) {
|
|
result = new ConcurrentHashMap<Lock, Integer>();
|
|
map.put(thread, result);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static Map<Lock, Integer> getLockCounts(final Thread thread) {
|
|
return getOrCreateMapForThread(thread, lockCounts);
|
|
}
|
|
|
|
private static Map<Lock, Integer> getPropagationCounts(final Thread thread) {
|
|
return getOrCreateMapForThread(thread, propagationCounts);
|
|
}
|
|
|
|
private static Map<Lock, Integer> getVirtualLockCounts(final Thread thread) {
|
|
return getOrCreateMapForThread(thread, virtualLockCounts);
|
|
}
|
|
|
|
private static void incrementLockCountForCurrentThread(Lock lock) {
|
|
Map<Lock, Integer> map = getCurrentThreadsLockCounts();
|
|
increment(lock, map);
|
|
}
|
|
|
|
private static void decrementLockCountForCurrentThread(Lock lock) {
|
|
decrement(lock, getCurrentThreadsLockCounts());
|
|
}
|
|
|
|
private static void incrementVirtualLockCountForCurrentThread(Lock lock) {
|
|
increment(lock, getCurrentThreadsVirtualLockCounts());
|
|
}
|
|
|
|
private static void decrementVirtualLockCountForCurrentThread(Lock lock) {
|
|
decrement(lock, getCurrentThreadsVirtualLockCounts());
|
|
}
|
|
|
|
private static void increment(Lock lock, Map<Lock, Integer> map) {
|
|
final int newValue;
|
|
if (map.containsKey(lock)) {
|
|
newValue = map.get(lock) + 1;
|
|
} else {
|
|
newValue = 1;
|
|
}
|
|
map.put(lock, newValue);
|
|
}
|
|
|
|
private static void decrement(Lock lock, Map<Lock, Integer> map) {
|
|
assert map.containsKey(lock);
|
|
final int newValue = map.get(lock) - 1;
|
|
if (newValue == 0) {
|
|
map.remove(lock);
|
|
} else {
|
|
map.put(lock, newValue);
|
|
}
|
|
}
|
|
|
|
private static boolean isInCurrentThreadsLockSet(Lock lock) {
|
|
return getCurrentThreadsLockCounts().containsKey(lock) || getCurrentThreadsVirtualLockCounts().containsKey(lock);
|
|
}
|
|
|
|
|
|
private static String formatStackTrace(StackTraceElement[] stackTrace) {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (StackTraceElement sf : stackTrace) {
|
|
sb.append(sf.toString());
|
|
sb.append('\n');
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
private static String getCurrentStackTrace() {
|
|
return formatStackTrace(Thread.currentThread().getStackTrace());
|
|
}
|
|
|
|
/**
|
|
* Actually obtains read or write lock <code>lock</code> of the {@link NamedReentrantReadWriteLock} owner lock.<p>
|
|
*
|
|
* Bug <a href="http://bugs.sun.com/view_bug.do?bug_id=6822370">http://bugs.sun.com/view_bug.do?bug_id=6822370</a> seems
|
|
* dangerous, particularly if it happens in a <code>LiveLeaderboardUpdater</code> thread. Even though the bug is reported to
|
|
* have been fixed in JDK 7(b79) we should be careful. This method tries to acquire a lock, allowing for five seconds to pass.
|
|
* After five seconds and not having retrieved the lock, tries again until the lock has been acquired.
|
|
*/
|
|
private static boolean lock(Lock lock, String lockDescriptionForTimeoutLogMessage, NamedReentrantReadWriteLock lockParent) {
|
|
boolean locked = false;
|
|
try {
|
|
locked = lock.tryLock(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK, TimeUnit.SECONDS);
|
|
if (!locked) {
|
|
Thread writer = lockParent.getWriter();
|
|
// capture the stack traces as quickly as possible to try to reflect the situation as it was when the lock couuldn't be obtained
|
|
StackTraceElement[] writerStackTrace = writer != null ? writer.getStackTrace() : null;
|
|
Map<Thread, StackTraceElement[]> readerStackTraces = new HashMap<Thread, StackTraceElement[]>();
|
|
final List<Thread> readers = lockParent.getReaders();
|
|
for (Thread reader : readers) {
|
|
readerStackTraces.put(reader, reader.getStackTrace());
|
|
}
|
|
StringBuilder message = new StringBuilder();
|
|
message.append("Couldn't acquire lock ");
|
|
message.append(lockDescriptionForTimeoutLogMessage);
|
|
message.append(" in ");
|
|
message.append(NUMBER_OF_SECONDS_TO_WAIT_FOR_LOCK);
|
|
message.append("s in thread " + Thread.currentThread().getName() + " at ");
|
|
message.append(getCurrentStackTrace());
|
|
if (writer != null) {
|
|
message.append("\nThe current writer is:\n");
|
|
appendThreadData(message, writer, writerStackTrace);
|
|
}
|
|
if (readers != null && !readers.isEmpty()) {
|
|
message.append("\nThe current readers are:\n");
|
|
for (Thread reader : readers) {
|
|
appendThreadData(message, reader, readerStackTraces.get(reader));
|
|
}
|
|
}
|
|
message.append("Trying again...");
|
|
logger.info(message.toString());
|
|
}
|
|
} catch (InterruptedException ex) {
|
|
logger.log(Level.WARNING, "Interrupted while waiting for lock "+lockDescriptionForTimeoutLogMessage, ex);
|
|
}
|
|
return locked;
|
|
}
|
|
|
|
private static void appendThreadData(StringBuilder message, Thread writer, StackTraceElement[] stackTrace) {
|
|
message.append(writer);
|
|
message.append('\n');
|
|
message.append(formatStackTrace(stackTrace));
|
|
message.append('\n');
|
|
}
|
|
|
|
}
|