de.thksystems.util.concurrent.Locker Maven / Gradle / Ivy
Show all versions of cumin Show documentation
/*
* 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.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.thksystems.util.function.CheckedSupplier;
/**
* Locking util.
*/
public final class Locker {
private static final Logger LOG = LoggerFactory.getLogger(Locker.class);
/** 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 HashMap<>();
/** Count of locks by current locking thread. */
private final Map lockCounts = new HashMap<>();
/** Gets the waiting queue for the given element. */
private synchronized Queue getThreadQueueForElement(T element, boolean addIfMissing) {
LOG.trace("Getting thread-queue for element: {}. Add if missing: {}", element, addIfMissing);
Queue threadQueue = threadQueueMap.get(element);
if (threadQueue == null && addIfMissing) {
LOG.trace("Adding thread-queue for element: {}", element);
threadQueue = new LinkedList<>();
threadQueueMap.put(element, threadQueue);
lockCounts.put(element, 1L);
}
return threadQueue;
}
/**
* Tries 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.error(msg, e);
throw new RuntimeException(msg, e);
}
}
/**
* Tries to lock the given element. It will be locked, if it is not locked by another thread.
* If it is locked, by another element, the supplied exception is thrown.
*/
public void tryLock(T element, Supplier exceptionSupplier) throws E {
boolean lockSucceeded = tryLock(element);
if (!lockSucceeded) {
LOG.trace("Try lock failed. Throwing exception.");
throw exceptionSupplier.get();
}
}
/**
* {@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.error(msg, e);
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.debug("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 '{}' is already locked by: {}", element, getLockingThread(element));
return false;
}
LOG.info("Waiting for lock of '{}'. Locked by: {}", element, 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.info(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.debug("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.debug("Added thread '{}' to waiting queue for '{}'", currentThread, element);
}
}
}
private void removeFromThreadQueueUnsafe(T element) {
Queue threadQueue = getThreadQueueForElement(element, false);
synchronized (threadQueue) {
Thread currentThread = Thread.currentThread();
LOG.debug("Removing thread '{}' from waiting queue for '{}'", currentThread, 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.debug("Unlocking: {}", element);
Queue threadQueue = getThreadQueueForElement(element, false);
if (threadQueue == null) {
LOG.warn("The element '{}' is NOT locked!", element);
return;
}
synchronized (threadQueue) {
Thread lockingThread = threadQueue.peek();
Thread currentThread = Thread.currentThread();
if (lockingThread != currentThread) {
LOG.warn("The element '{}' is NOT locked by the current thread '{}'. It is locked by thread '{}' -> IGNORED!", element, currentThread,
lockingThread);
return;
}
if (lockCounts.get(element) == 1) {
threadQueue.remove();
LOG.debug("Unlocked.");
} else {
lockCounts.merge(element, 1L, (old, dec) -> old - dec);
LOG.debug("Unlocking not possible, because locked more than onced. Decreased lock counter to {}", lockCounts.get(element));
}
}
}
/**
* 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);
}
}
}