All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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); } } }