001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005//     http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.ioc.internal.util;
014
015import org.apache.tapestry5.ioc.Invokable;
016
017import java.util.concurrent.TimeUnit;
018import java.util.concurrent.locks.ReadWriteLock;
019import java.util.concurrent.locks.ReentrantReadWriteLock;
020
021/**
022 * A barrier used to execute code in a context where it is guarded by read/write locks. In addition, handles upgrading
023 * read locks to write locks (and vice versa). Execution of code within a lock is in terms of a {@link Runnable} object
024 * (that returns no value), or a {@link Invokable} object (which does return a value).
025 */
026public class ConcurrentBarrier
027{
028    private final ReadWriteLock lock = new ReentrantReadWriteLock();
029
030    /**
031     * This is, of course, a bit of a problem. We don't have an avenue for ensuring that this ThreadLocal is destroyed
032     * at the end of the request, and that means a thread can hold a reference to the class and the class loader which
033     * loaded it. This may cause redeployment problems (leaked classes and class loaders). Apparently JDK 1.6 provides
034     * the APIs to check to see if the current thread has a read lock. So, we tend to remove the TL, rather than set its
035     * value to false.
036     */
037    private static class ThreadBoolean extends ThreadLocal<Boolean>
038    {
039        @Override
040        protected Boolean initialValue()
041        {
042            return false;
043        }
044    }
045
046    private final ThreadBoolean threadHasReadLock = new ThreadBoolean();
047
048    /**
049     * Invokes the object after acquiring the read lock (if necessary). If invoked when the read lock has not yet been
050     * acquired, then the lock is acquired for the duration of the call. If the lock has already been acquired, then the
051     * status of the lock is not changed.
052     *
053     * TODO: Check to see if the write lock is acquired and <em>not</em> acquire the read lock in that situation.
054     * Currently this code is not re-entrant. If a write lock is already acquired and the thread attempts to get the
055     * read lock, then the thread will hang. For the moment, all the uses of ConcurrentBarrier are coded in such a way
056     * that reentrant locks are not a problem.
057     *
058     * @param <T>
059     * @param invokable
060     * @return the result of invoking the invokable
061     */
062    public <T> T withRead(Invokable<T> invokable)
063    {
064        boolean readLockedAtEntry;
065
066        synchronized (threadHasReadLock)
067        {
068            readLockedAtEntry = threadHasReadLock.get();
069        }
070
071        if (!readLockedAtEntry)
072        {
073            lock.readLock().lock();
074
075            synchronized (threadHasReadLock)
076            {
077                threadHasReadLock.set(true);
078            }
079        }
080
081        try
082        {
083            return invokable.invoke();
084        }
085        finally
086        {
087            if (!readLockedAtEntry)
088            {
089                lock.readLock().unlock();
090
091                synchronized (threadHasReadLock)
092                {
093                    threadHasReadLock.remove();
094                }
095            }
096        }
097    }
098
099    /**
100     * As with {@link #withRead(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
101     */
102    public void withRead(final Runnable runnable)
103    {
104        Invokable<Void> invokable = new Invokable<Void>()
105        {
106            @Override
107            public Void invoke()
108            {
109                runnable.run();
110
111                return null;
112            }
113        };
114
115        withRead(invokable);
116    }
117
118    /**
119     * Acquires the exclusive write lock before invoking the Invokable. The code will be executed exclusively, no other
120     * reader or writer threads will exist (they will be blocked waiting for the lock). If the current thread has a read
121     * lock, it is released before attempting to acquire the write lock, and re-acquired after the write lock is
122     * released. Note that in that short window, between releasing the read lock and acquiring the write lock, it is
123     * entirely possible that some other thread will sneak in and do some work, so the {@link Invokable} object should
124     * be prepared for cases where the state has changed slightly, despite holding the read lock. This usually manifests
125     * as race conditions where either a) some parallel unrelated bit of work has occured or b) duplicate work has
126     * occured. The latter is only problematic if the operation is very expensive.
127     *
128     * @param <T>
129     * @param invokable
130     */
131    public <T> T withWrite(Invokable<T> invokable)
132    {
133        boolean readLockedAtEntry = releaseReadLock();
134
135        lock.writeLock().lock();
136
137        try
138        {
139            return invokable.invoke();
140        }
141        finally
142        {
143            lock.writeLock().unlock();
144            restoreReadLock(readLockedAtEntry);
145        }
146    }
147
148    private boolean releaseReadLock()
149    {
150        boolean readLockedAtEntry;
151
152        synchronized (threadHasReadLock)
153        {
154            readLockedAtEntry = threadHasReadLock.get();
155        }
156
157        if (readLockedAtEntry)
158        {
159            lock.readLock().unlock();
160
161            synchronized (threadHasReadLock)
162            {
163                threadHasReadLock.set(false);
164            }
165        }
166
167        return readLockedAtEntry;
168    }
169
170    private void restoreReadLock(boolean readLockedAtEntry)
171    {
172        if (readLockedAtEntry)
173        {
174            lock.readLock().lock();
175
176            synchronized (threadHasReadLock)
177            {
178                threadHasReadLock.set(true);
179            }
180        }
181        else
182        {
183            synchronized (threadHasReadLock)
184            {
185                threadHasReadLock.remove();
186            }
187        }
188    }
189
190    /**
191     * As with {@link #withWrite(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
192     */
193    public void withWrite(final Runnable runnable)
194    {
195        Invokable<Void> invokable = new Invokable<Void>()
196        {
197            @Override
198            public Void invoke()
199            {
200                runnable.run();
201
202                return null;
203            }
204        };
205
206        withWrite(invokable);
207    }
208
209    /**
210     * Try to aquire the exclusive write lock and invoke the Runnable. If the write lock is obtained within the specfied
211     * timeout, then this method behaves as {@link #withWrite(Runnable)} and will return true. If the write lock is not
212     * obtained within the timeout then the runnable is never invoked and the method will return false.
213     *
214     * @param runnable    Runnable object to execute inside the write lock.
215     * @param timeout     Time to wait for write lock.
216     * @param timeoutUnit Units of timeout.
217     * @return true if lock was obtained and the runnable executed, or false otherwise.
218     */
219    public boolean tryWithWrite(final Runnable runnable, long timeout, TimeUnit timeoutUnit)
220    {
221        boolean readLockedAtEntry = releaseReadLock();
222
223        boolean obtainedLock = false;
224
225        try
226        {
227            try
228            {
229                obtainedLock = lock.writeLock().tryLock(timeout, timeoutUnit);
230
231                if (obtainedLock) runnable.run();
232
233            }
234            catch (InterruptedException e)
235            {
236                obtainedLock = false;
237            }
238            finally
239            {
240                if (obtainedLock) lock.writeLock().unlock();
241            }
242        }
243        finally
244        {
245            restoreReadLock(readLockedAtEntry);
246        }
247
248        return obtainedLock;
249    }
250
251}