de.thksystems.util.concurrent.Locker Maven / Gradle / Ivy
/*
* tksCommons
*
* Author : Thomas Kuhlmann (ThK-Systems, http://www.thk-systems.de)
* License : LGPL (https://www.gnu.org/licenses/lgpl.html)
*/
package de.thksystems.util.concurrent;
import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.WeakHashMap;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import java.util.logging.Logger;
import de.thksystems.util.function.CheckedSupplier;
/**
* Locking util.
*/
public final class Locker {
private static final Logger LOG = Logger.getLogger(Locker.class.getName());
/** Queue of threads for element. The first entry is the current locking one. Accesses to this map must be synchronized. */
private final Map> threadQueueMap = new WeakHashMap<>();
/** Count of locks by current locking thread. */
private final Map lockCounts = new WeakHashMap<>();
/** Gets the waiting queue for the given element. */
private synchronized Queue getThreadQueueForElement(T element, boolean addIfMissing) {
Queue threadQueue = threadQueueMap.get(element);
if (threadQueue == null && addIfMissing) {
threadQueue = new LinkedList<>();
threadQueueMap.put(element, threadQueue);
lockCounts.put(element, 1L);
}
return threadQueue;
}
/**
* Trys to lock the given element. It will be locked, if it is not locked by another thread.
*
* @return true
in case of a succeeded lock, false
otherwise
*/
public boolean tryLock(T element) {
try {
return lock(element, Optional.empty(), Optional.of(Boolean.TRUE));
} catch (TimeoutException e) {
// Must not happen here
String msg = "Unexpected timeout exception: " + e.getMessage();
LOG.severe(msg);
throw new RuntimeException(msg, e);
}
}
/**
* {@link Locker#lock(Object, long)} using an (almost) infinite waiting time.
*/
public void lock(T element) {
try {
lock(element, Optional.empty(), Optional.empty());
} catch (TimeoutException e) {
// Must not happen here
String msg = "Unexpected timeout exception: " + e.getMessage();
LOG.severe(msg);
throw new RuntimeException(msg, e);
}
}
/**
* {@link Locker#lock(Object, long)} with a mandatory waiting time in milliseconds.
*/
public void lock(T element, long maxWaitTime) throws TimeoutException {
lock(element, Optional.of(maxWaitTime), Optional.empty());
}
/**
* Locks the given element for the current thread.
* If it is already locked (for another thread), it waits for unlock.
* If the waiting time exceeds the given one, an {@link TimeoutException} is thrown.
*/
@SuppressWarnings("unchecked")
protected boolean lock(T element, Optional optionalMaxWaitTime, Optional tryLock) throws TimeoutException {
LOG.fine("Locking: " + element);
// We need a unique string (if element if of type String)
if (element instanceof String) {
element = (T) ((String) element).intern(); // NOSONAR
}
// Adding to thread queue; This creates the (first) lock, if the queue was empty before
addToThreadQueue(element);
// Check for lock
long startTime = System.currentTimeMillis();
if (isLocked(element)) {
if (tryLock.orElse(Boolean.FALSE)) {
removeFromThreadQueueUnsafe(element);
LOG.info("Element '" + element + "' is already locked by: " + getLockingThread(element));
return false;
}
LOG.info("Waiting for lock of '" + element + "'. Locked by: " + getLockingThread(element));
}
// Wait for lock (by other thread), if any
long maxWaitTime = optionalMaxWaitTime.orElse(1_000L * 60L * 24L * 365L * 1_000_000_000L); // ~1 billion years
while (isLocked(element) && (System.currentTimeMillis() <= startTime + maxWaitTime)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// Interruption here is okay.
}
}
// Check if time exceeded while waiting on lock
if (isLocked(element) && (System.currentTimeMillis() > startTime + maxWaitTime)) {
removeFromThreadQueueUnsafe(element);
String msg = String.format("Time (%d ms) exceeded for waiting on locked '%s'. Locked by: %s", maxWaitTime, element, getLockingThread(element));
LOG.severe(msg);
throw new TimeoutException(msg);
}
return true; // Element was locked
}
/**
* Add current thread to queue of waiting threads for given element.
*/
protected void addToThreadQueue(T element) {
// Create map entry in waiting queue for element, if needed
Queue threadQueue = getThreadQueueForElement(element, true);
synchronized (threadQueue) {
// Check, if element is locked by current thread, then increase counter
if (isHeldByCurrentThread(element)) {
lockCounts.merge(element, 1L, (old, inc) -> old + inc);
LOG.fine("Element is already locked by current thread. Increased lock count: " + lockCounts.get(element));
}
// If element is not held by current thread, add it to the waiting queue.
else {
Thread currentThread = Thread.currentThread();
threadQueue.add(currentThread);
LOG.fine("Added thread '" + currentThread + "' to waiting queue for '" + element + "'");
}
}
}
private void removeFromThreadQueueUnsafe(T element) {
Queue threadQueue = getThreadQueueForElement(element, false);
synchronized (threadQueue) {
Thread currentThread = Thread.currentThread();
LOG.fine("Removing thread '" + currentThread + "' from waiting queue for '" + element + "'");
threadQueue.remove(currentThread);
}
}
/**
* Unlocks the given element. (If it is not locked by the current thread, it will not be unlocked. No exception is thrown in this case, just logging.)
*
* It is null-safe, because it may be used in finally blocks.
*/
public void unlock(T element) {
if (element == null) {
return;
}
LOG.fine("Unlocking: " + element);
Queue threadQueue = getThreadQueueForElement(element, false);
if (threadQueue == null) {
LOG.warning("The element '" + element + "' is NOT locked!");
return;
}
synchronized (threadQueue) {
Thread lockingThread = threadQueue.peek();
Thread currentThread = Thread.currentThread();
if (lockingThread != currentThread) {
LOG.warning("The element '" + element + "' is NOT locked by the current thread '" + currentThread + "'. It is locked by thread '"
+ lockingThread + "' -> IGNORED!");
return;
}
if (lockCounts.get(element) == 1) {
threadQueue.remove();
LOG.fine("Unlocked.");
} else {
lockCounts.merge(element, 1L, (old, dec) -> old - dec);
LOG.fine("Unlocking not possible, because locked more than onced. Decreased lock counter.");
}
}
}
/**
* Returns true
, if locked (by another thread).
*/
public boolean isLocked(T element) {
if (threadQueueMap.containsKey(element)) {
Thread thread = threadQueueMap.get(element).peek();
return thread != null && thread != Thread.currentThread();
}
return false;
}
/**
* Return true
, if locked by current thread.
*/
public boolean isHeldByCurrentThread(T element) {
if (threadQueueMap.containsKey(element)) {
Thread thread = threadQueueMap.get(element).peek();
return thread != null && thread == Thread.currentThread();
}
return false;
}
/**
* Get currently locking thread.
*/
public Thread getLockingThread(T element) {
return threadQueueMap.containsKey(element) ? threadQueueMap.get(element).peek() : null;
}
/**
* Locks element, than executes given {@link Runnable} and finally unlocks element. (Execute-around-method-pattern.)
*/
public void executeWithLock(T element, Runnable task) {
lock(element);
try {
task.run();
} finally {
unlock(element);
}
}
/**
* Try to lock element using {@link #tryLock(Object)}. If succeeded call onNotLocked {@link Runnable}, if locked call onLocked {@link Runnable}.
*/
public void executeWithLock(T element, Runnable onNotLocked, Runnable onLocked) {
try {
if (tryLock(element)) {
onNotLocked.run();
} else {
onLocked.run();
}
} finally {
unlock(element);
}
}
/**
* Try to lock element using {@link #tryLock(Object)}. If succeeded call {@link Runnable}, if locked throw Exception.
*/
public void executeWithLock(T element, Runnable task, X exception) throws X {
try {
if (tryLock(element)) {
task.run();
} else {
throw exception;
}
} finally {
unlock(element);
}
}
/**
* Locks element, than executes given {@link Supplier}, returns its result and finally unlocks element. (Execute-around-method-pattern.)
*/
public S executeWithLock(T element, Supplier supplier) {
lock(element);
try {
return supplier.get();
} finally {
unlock(element);
}
}
/**
* Try to lock element using {@link #tryLock(Object)}. If succeeded call onNotLocked {@link Supplier}, if locked call onLocked {@link Supplier}.
*/
public S executeWithLock(T element, Supplier onNotLocked, Supplier onLocked) {
try {
if (tryLock(element)) {
return onNotLocked.get();
} else {
return onLocked.get();
}
} finally {
unlock(element);
}
}
/**
* Try to lock element using {@link #tryLock(Object)}. If succeeded call {@link Supplier}, if locked throw Exception.
*/
public S executeWithLock(T element, Supplier supplier, X exception) throws X {
try {
if (tryLock(element)) {
return supplier.get();
} else {
throw exception;
}
} finally {
unlock(element);
}
}
/**
* Try to lock element using {@link #tryLock(Object)}. If succeeded call {@link CheckedSupplier}, if locked throw Exception.
*/
public S executeCheckedWithLock(T element, CheckedSupplier supplier, X exception) throws X, Y {
try {
if (tryLock(element)) {
return supplier.get();
} else {
throw exception;
}
} finally {
unlock(element);
}
}
}