    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}