create changelog entry
[debian/openrocket] / core / src / net / sf / openrocket / util / SafetyMutex.java
1 package net.sf.openrocket.util;
2
3 import java.util.LinkedList;
4
5 import net.sf.openrocket.logging.LogHelper;
6 import net.sf.openrocket.logging.TraceException;
7 import net.sf.openrocket.startup.Application;
8
9 /**
10  * A mutex that can be used for verifying thread safety.  This class cannot be
11  * used to perform synchronization, only to detect concurrency issues.  This
12  * class can be used by the main methods of non-thread-safe classes to ensure
13  * the class is not wrongly used from multiple threads concurrently.
14  * <p>
15  * This mutex is not reentrant even for the same thread that has locked it.
16  * 
17  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
18  */
19 public abstract class SafetyMutex {
20         private static final boolean USE_CHECKS = Application.useSafetyChecks();
21         private static final LogHelper log = Application.getLogger();
22         
23         
24         /**
25          * Return a new instance of a safety mutex.  This returns an actual implementation
26          * or a bogus implementation depending on whether safety checks are enabled or disabled.
27          * 
28          * @return      a new instance of a safety mutex
29          */
30         public static SafetyMutex newInstance() {
31                 if (USE_CHECKS) {
32                         return new ConcreteSafetyMutex();
33                 } else {
34                         return new BogusSafetyMutex();
35                 }
36         }
37         
38
39         /**
40          * Verify that this mutex is unlocked, but don't lock it.  This has the same effect
41          * as <code>mutex.lock(); mutex.unlock();</code> and is useful for methods that return
42          * immediately (e.g. getters).
43          * 
44          * @throws ConcurrencyException if this mutex is already locked.
45          */
46         public abstract void verify();
47         
48         
49         /**
50          * Lock this mutex.  If this mutex is already locked an error is raised and
51          * a ConcurrencyException is thrown.  The location parameter is used to distinguish
52          * the locking location, and it should be e.g. the method name.
53          * 
54          * @param location      a string describing the location where this mutex was locked (cannot be null).
55          * 
56          * @throws ConcurrencyException         if this mutex is already locked.
57          */
58         public abstract void lock(String location);
59         
60         
61         /**
62          * Unlock this mutex.  If this mutex is not locked at the position of the parameter
63          * or was locked by another thread than the current thread an error is raised,
64          * but an exception is not thrown.
65          * <p>
66          * This method is guaranteed never to throw an exception, so it can safely be used in finally blocks.
67          * 
68          * @param location      a location string matching that which locked the mutex
69          * @return                      whether the unlocking was successful (this normally doesn't need to be checked)
70          */
71         public abstract boolean unlock(String location);
72         
73         
74
75         /**
76          * Bogus implementation of a safety mutex (used when safety checking is not performed).
77          */
78         static class BogusSafetyMutex extends SafetyMutex {
79                 
80                 @Override
81                 public void verify() {
82                 }
83                 
84                 @Override
85                 public void lock(String location) {
86                 }
87                 
88                 @Override
89                 public boolean unlock(String location) {
90                         return true;
91                 }
92                 
93         }
94         
95         /**
96          * A concrete, working implementation of a safety mutex.
97          */
98         static class ConcreteSafetyMutex extends SafetyMutex {
99                 private static final boolean STORE_LOCKING_LOCATION = (System.getProperty("openrocket.debug.mutexlocation") != null);
100                 
101                 // Package-private for unit testing
102                 static volatile boolean errorReported = false;
103                 
104                 // lockingThread is set when this mutex is locked.
105                 Thread lockingThread = null;
106                 // longingLocation is set when lockingThread is, if STORE_LOCKING_LOCATION is true
107                 TraceException lockingLocation = null;
108                 // Stack of places that have locked this mutex
109                 final LinkedList<String> locations = new LinkedList<String>();
110                 
111                 
112
113                 @Override
114                 public synchronized void verify() {
115                         checkState(true);
116                         if (lockingThread != null && lockingThread != Thread.currentThread()) {
117                                 error("Mutex is already locked", true);
118                         }
119                 }
120                 
121                 
122
123                 @Override
124                 public synchronized void lock(String location) {
125                         if (location == null) {
126                                 throw new IllegalArgumentException("location is null");
127                         }
128                         checkState(true);
129                         
130                         Thread currentThread = Thread.currentThread();
131                         if (lockingThread != null && lockingThread != currentThread) {
132                                 error("Mutex is already locked", true);
133                         }
134                         
135                         lockingThread = currentThread;
136                         if (STORE_LOCKING_LOCATION) {
137                                 lockingLocation = new TraceException("Location where mutex was locked '" + location + "'");
138                         }
139                         locations.push(location);
140                 }
141                 
142                 
143
144
145                 @Override
146                 public synchronized boolean unlock(String location) {
147                         try {
148                                 
149                                 if (location == null) {
150                                         Application.getExceptionHandler().handleErrorCondition("location is null");
151                                         location = "";
152                                 }
153                                 checkState(false);
154                                 
155
156                                 // Check that the mutex is locked
157                                 if (lockingThread == null) {
158                                         error("Mutex was not locked", false);
159                                         return false;
160                                 }
161                                 
162                                 // Check that the mutex is locked by the current thread
163                                 if (lockingThread != Thread.currentThread()) {
164                                         error("Mutex is being unlocked from differerent thread than where it was locked", false);
165                                         return false;
166                                 }
167                                 
168                                 // Check that the unlock location is correct
169                                 String lastLocation = locations.pop();
170                                 if (!location.equals(lastLocation)) {
171                                         locations.push(lastLocation);
172                                         error("Mutex unlocking location does not match locking location, location=" + location, false);
173                                         return false;
174                                 }
175                                 
176                                 // Unlock the mutex if the last one
177                                 if (locations.isEmpty()) {
178                                         lockingThread = null;
179                                         lockingLocation = null;
180                                 }
181                                 return true;
182                         } catch (Exception e) {
183                                 Application.getExceptionHandler().handleErrorCondition("An exception occurred while unlocking a mutex, " +
184                                                 "locking thread=" + lockingThread + " locations=" + locations, e);
185                                 return false;
186                         }
187                 }
188                 
189                 
190
191                 /**
192                  * Check that the internal state of the mutex (lockingThread vs. locations) is correct.
193                  */
194                 private void checkState(boolean throwException) {
195                         /*
196                          * Disallowed states:
197                          *   lockingThread == null  &&  !locations.isEmpty()
198                          *   lockingThread != null  &&  locations.isEmpty()
199                          */
200                         if ((lockingThread == null) ^ (locations.isEmpty())) {
201                                 // Clear the mutex only after error() has executed (and possibly thrown an exception)
202                                 try {
203                                         error("Mutex data inconsistency occurred - unlocking mutex", throwException);
204                                 } finally {
205                                         lockingThread = null;
206                                         lockingLocation = null;
207                                         locations.clear();
208                                 }
209                         }
210                 }
211                 
212                 
213                 /**
214                  * Raise an error.  The first occurrence is passed directly to the exception handler,
215                  * later errors are simply logged.
216                  */
217                 private void error(String message, boolean throwException) {
218                         message = message +
219                                         ", current thread = " + Thread.currentThread() +
220                                         ", locking thread=" + lockingThread +
221                                         ", locking locations=" + locations;
222                         
223                         ConcurrencyException ex = new ConcurrencyException(message, lockingLocation);
224                         
225                         if (!errorReported) {
226                                 errorReported = true;
227                                 Application.getExceptionHandler().handleErrorCondition(ex);
228                         } else {
229                                 log.error(message, ex);
230                         }
231                         
232                         if (throwException) {
233                                 throw ex;
234                         }
235                 }
236         }
237 }