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