package net.sf.openrocket.util; import java.util.LinkedList; import net.sf.openrocket.startup.Application; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A mutex that can be used for verifying thread safety. This class cannot be * used to perform synchronization, only to detect concurrency issues. This * class can be used by the main methods of non-thread-safe classes to ensure * the class is not wrongly used from multiple threads concurrently. * <p> * This mutex is not reentrant even for the same thread that has locked it. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public abstract class SafetyMutex { private static final boolean USE_CHECKS = Application.useSafetyChecks(); private static final Logger log = LoggerFactory.getLogger(SafetyMutex.class); /** * Return a new instance of a safety mutex. This returns an actual implementation * or a bogus implementation depending on whether safety checks are enabled or disabled. * * @return a new instance of a safety mutex */ public static SafetyMutex newInstance() { if (USE_CHECKS) { return new ConcreteSafetyMutex(); } else { return new BogusSafetyMutex(); } } /** * Verify that this mutex is unlocked, but don't lock it. This has the same effect * as <code>mutex.lock(); mutex.unlock();</code> and is useful for methods that return * immediately (e.g. getters). * * @throws ConcurrencyException if this mutex is already locked. */ public abstract void verify(); /** * Lock this mutex. If this mutex is already locked an error is raised and * a ConcurrencyException is thrown. The location parameter is used to distinguish * the locking location, and it should be e.g. the method name. * * @param location a string describing the location where this mutex was locked (cannot be null). * * @throws ConcurrencyException if this mutex is already locked. */ public abstract void lock(String location); /** * Unlock this mutex. If this mutex is not locked at the position of the parameter * or was locked by another thread than the current thread an error is raised, * but an exception is not thrown. * <p> * This method is guaranteed never to throw an exception, so it can safely be used in finally blocks. * * @param location a location string matching that which locked the mutex * @return whether the unlocking was successful (this normally doesn't need to be checked) */ public abstract boolean unlock(String location); /** * Bogus implementation of a safety mutex (used when safety checking is not performed). */ static class BogusSafetyMutex extends SafetyMutex { @Override public void verify() { } @Override public void lock(String location) { } @Override public boolean unlock(String location) { return true; } } /** * A concrete, working implementation of a safety mutex. */ static class ConcreteSafetyMutex extends SafetyMutex { private static final boolean STORE_LOCKING_LOCATION = (System.getProperty("openrocket.debug.mutexlocation") != null); // Package-private for unit testing static volatile boolean errorReported = false; // lockingThread is set when this mutex is locked. Thread lockingThread = null; // longingLocation is set when lockingThread is, if STORE_LOCKING_LOCATION is true Throwable lockingLocation = null; // Stack of places that have locked this mutex final LinkedList<String> locations = new LinkedList<String>(); @Override public synchronized void verify() { checkState(true); if (lockingThread != null && lockingThread != Thread.currentThread()) { error("Mutex is already locked", true); } } @Override public synchronized void lock(String location) { if (location == null) { throw new IllegalArgumentException("location is null"); } checkState(true); Thread currentThread = Thread.currentThread(); if (lockingThread != null && lockingThread != currentThread) { error("Mutex is already locked", true); } lockingThread = currentThread; if (STORE_LOCKING_LOCATION) { lockingLocation = new Throwable("Location where mutex was locked '" + location + "'"); } locations.push(location); } @Override public synchronized boolean unlock(String location) { try { if (location == null) { Application.getExceptionHandler().handleErrorCondition("location is null"); location = ""; } checkState(false); // Check that the mutex is locked if (lockingThread == null) { error("Mutex was not locked", false); return false; } // Check that the mutex is locked by the current thread if (lockingThread != Thread.currentThread()) { error("Mutex is being unlocked from differerent thread than where it was locked", false); return false; } // Check that the unlock location is correct String lastLocation = locations.pop(); if (!location.equals(lastLocation)) { locations.push(lastLocation); error("Mutex unlocking location does not match locking location, location=" + location, false); return false; } // Unlock the mutex if the last one if (locations.isEmpty()) { lockingThread = null; lockingLocation = null; } return true; } catch (Exception e) { Application.getExceptionHandler().handleErrorCondition("An exception occurred while unlocking a mutex, " + "locking thread=" + lockingThread + " locations=" + locations, e); return false; } } /** * Check that the internal state of the mutex (lockingThread vs. locations) is correct. */ private void checkState(boolean throwException) { /* * Disallowed states: * lockingThread == null && !locations.isEmpty() * lockingThread != null && locations.isEmpty() */ if ((lockingThread == null) ^ (locations.isEmpty())) { // Clear the mutex only after error() has executed (and possibly thrown an exception) try { error("Mutex data inconsistency occurred - unlocking mutex", throwException); } finally { lockingThread = null; lockingLocation = null; locations.clear(); } } } /** * Raise an error. The first occurrence is passed directly to the exception handler, * later errors are simply logged. */ private void error(String message, boolean throwException) { message = message + ", current thread = " + Thread.currentThread() + ", locking thread=" + lockingThread + ", locking locations=" + locations; ConcurrencyException ex = new ConcurrencyException(message, lockingLocation); if (!errorReported) { errorReported = true; Application.getExceptionHandler().handleErrorCondition(ex); } else { log.error(message, ex); } if (throwException) { throw ex; } } } }