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            public Void invoke()
109            {
110                runnable.run();
111
112                return null;
113            }
114        };
115
116        withRead(invokable);
117    }
118
119    /**
120     * Acquires the exclusive write lock before invoking the Invokable. The code will be executed exclusively, no other
121     * reader or writer threads will exist (they will be blocked waiting for the lock). If the current thread has a read
122     * lock, it is released before attempting to acquire the write lock, and re-acquired after the write lock is
123     * released. Note that in that short window, between releasing the read lock and acquiring the write lock, it is
124     * entirely possible that some other thread will sneak in and do some work, so the {@link Invokable} object should
125     * be prepared for cases where the state has changed slightly, despite holding the read lock. This usually manifests
126     * as race conditions where either a) some parallel unrelated bit of work has occured or b) duplicate work has
127     * occured. The latter is only problematic if the operation is very expensive.
128     *
129     * @param <T>
130     * @param invokable
131     */
132    public <T> T withWrite(Invokable<T> invokable)
133    {
134        boolean readLockedAtEntry = releaseReadLock();
135
136        lock.writeLock().lock();
137
138        try
139        {
140            return invokable.invoke();
141        }
142        finally
143        {
144            lock.writeLock().unlock();
145            restoreReadLock(readLockedAtEntry);
146        }
147    }
148
149    private boolean releaseReadLock()
150    {
151        boolean readLockedAtEntry;
152
153        synchronized (threadHasReadLock)
154        {
155            readLockedAtEntry = threadHasReadLock.get();
156        }
157
158        if (readLockedAtEntry)
159        {
160            lock.readLock().unlock();
161
162            synchronized (threadHasReadLock)
163            {
164                threadHasReadLock.set(false);
165            }
166        }
167
168        return readLockedAtEntry;
169    }
170
171    private void restoreReadLock(boolean readLockedAtEntry)
172    {
173        if (readLockedAtEntry)
174        {
175            lock.readLock().lock();
176
177            synchronized (threadHasReadLock)
178            {
179                threadHasReadLock.set(true);
180            }
181        }
182        else
183        {
184            synchronized (threadHasReadLock)
185            {
186                threadHasReadLock.remove();
187            }
188        }
189    }
190
191    /**
192     * As with {@link #withWrite(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
193     */
194    public void withWrite(final Runnable runnable)
195    {
196        Invokable<Void> invokable = new Invokable<Void>()
197        {
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 & runnabled executed. 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}