Skip to main content

Extending the ReentrantReadWriteLock

June 28, 2007

{cs.r.title}



JDK 1.5 brought us the wonderful world of thread control, thread
pooling, and sophisticated locking mechanisms, but with it came a
lot of headaches for developers. There was a time when all you
needed to do to control your threads was to learn about the
synchronized keyword and maybe about
wait() and notify(). You can and should
still use these, of course, but advanced developers will want to
use the java.util.concurrent classes to gain more
control over their multithreaded applications.

This article will not cover the
java.util.concurrent package; nor will it give you a
tutorial on how to use it. It will, however, talk about a specific
mechanism, and suggest a way to extend this mechanism and add more
power to it.

The ReentrantReadWriteLock Class

A read-write lock mechanism is a great way to optimize
the performance of locking applications. Every application that uses
resources in a multithreaded environment (pretty much any server,
for example) must use some locking mechanism. A read-write lock
enables you to differentiate between read operations and write
operations. Basically it means that several read operations can
occur at the same time, but only a single write operation can be
invoked. In addition, when read operations occur, no write
operation can be invoked. What you get from this arrangement are
several threads that can preform the read job simultaneously.

In JDK 1.5, this is all built into the JDK, under the
java.util.concurrent.locks package. The
ReadWriteLock interface and the
ReentrantReadWriteLock class give you this wonderful
mechanism to use.

The Pitfalls

Every time you deal with threads, you need to be more careful
than usual. A lot of sleepless nights might have been prevented if
developers had been a bit more careful dealing with threading and
locking. The ReentrantReadWriteLock introduces some
pitfalls that you may want to avoid. Two of them will be discussed
here:

  1. Locking without unlocking
  2. Trying to write lock when holding a read lock

This article was not written just to tell you about these
pitfalls and send you on your way, but also to introduce a solution,
which comes as an extension to the
ReentrantReadWriteLock class. This extension (actually
a composition) will wrap the lock and add more capabilities to help
you overcome the specified problems.

Let's first talk a little bit about the problems. Locking
without unlocking is quite easy to understand. If you acquire a
lock and forget to unlock it, you will probably have a problem. Of
course, when I say "forget," I also mean "unintentionally
misplace;" for example, an exception might be thrown and "jump"
over your unlock call. Your application will keep running, but
sometime in the future, another thread will try to acquire the lock
and will be stuck forever.

The second problem is even harder to catch. It all starts with a
little innocent sentence in the Javadoc of the
ReentrantReadWriteLock class saying, "If a reader
tries to acquire the write lock it will never succeed." This means
that if a thread holds a read lock, and is trying to acquire a
write lock, it will just block forever. Don't forget that when
using these locking mechanisms, you might find yourself locking in
one method and unlocking in another method after doing a lot of
work in between, so this might happen to you.

Introducing the TimedReadWriteLock Class

After talking about the problems, let's talk about solutions.
The solution presented here is based on a new class, called
TimedReadWriteLock, that helps you discover whether
either of the previous problems has occurred and then eliminate the
problematic source. Since we deal with locking, this class usually
cannot fix the problem at runtime, but at least you will know what
the problem is and where it came from.

TimedReadWriteLock will add the following
capabilities:

  1. If a thread holds a lock for more than n milliseconds,
    you will get a notification in your log with the stack trace of the
    locker.
  2. If a thread that holds a read lock tries to acquire a write
    lock, it will fail and you will get a notification in your
    logs.

The class supports nested locking (in which a thread acquires a
lock more than once and releases it more than once) and downgrading
from write lock to read lock.

The basic functionality is achieved by adding a
Timer that checks the amount of time a lock is locked,
and a ThreadLocal stack of TimerTask
instances per locking thread. When the lock is locked, a
TimerTask starts counting the time it remains locked.
If it is not unlocked after a specified amount of time, an error is
logged with the locker's stack trace details. Since we will use a
thread-local stack, each thread will get is own timer tasks, and we
will also be able to find out if a thread is currently holding a
read lock and then prevent it form acquiring a write lock.

The TimedReadWriteLock Implementation

I have chosen not to extend the
ReentrantReadWriteLock class but instead to use
"http://en.wikipedia.org/wiki/Object_composition">composition,
and by that create my own API for working with the lock. You can,
if you want, extend the class and add similar functionality.

Let's start by defining our class:


public class TimedReadWriteLock  {

        private ReadWriteLock rwLock =
                        new ReentrantReadWriteLock(true);

        private long maxWait;
        private String name;

        // This is static so all the locks will use the same timer
        // thread
        private static Timer waitTimer = new Timer(true);

        private LockTaskStack lockTaskStack = new LockTaskStack();

        public TimedReadWriteLock(String name,long maxWait) {
                this.maxWait = maxWait;
                this.name = name;
        }
}

This class contains the ReadWriteLock instance, a
field indicating how long to wait to unlock, a name for the lock, a
static Timer instance (this field is static, so all the
locks will use a single thread), and LockTaskStack,
which is a private class that will be shown later. The constructor
is pretty straightforward, taking the name and waiting time as
arguments.

Two methods will control the locks:

  • void setWriteLock(boolean lock)
  • void setReadLock(boolean lock)

Both methods get a single Boolean argument that indicates
weather you want to lock or unlock. Basically, those two methods
are very similar in content, so I will show the first one as an
example of what should be inside. My guess is that you will be able
to figure out what should be in the second one (and if not, you are
welcome to look at the supplied full implementation under the
Resources section).

This is the method's body:


public void setWriteLock(boolean lock) {
        if (lock) {
                rwLock.writeLock().lock();
                WaitTimerTask job =
                        new WaitTimerTask(Thread.currentThread(),false);

                lockTaskStack.get().push(job);
                waitTimer.schedule(job, maxWait);
        } else {
                rwLock.writeLock().unlock();
                WaitTimerTask job = lockTaskStack.get().pop();
                job.cancel();
        }
}

Instead of just locking and unlocking, this method also manages
a stack of TimerTask instances, each pointing to a
locking thread and scheduled on the timer. When locking, a new task
is scheduled; when unlocking, the task is canceled. The reason for
using a stack is that a thread might use nested locking. The stack
is also a ThreadLocal object, since each thread should
be independent of other threads.

This is the WaitTimerTask private inner class:


private class WaitTimerTask extends TimerTask {
        private Thread locker;
        private boolean readLock;
        private StackTraceElement[] stackElements;

        WaitTimerTask(Thread locker,boolean readLock) {
                this.locker = locker;
                this.readLock = readLock;
                stackElements = locker.getStackTrace();
        }

        public void run() {
                String lockType = readLock ? "read" : "write";
                StringBuilder msg = new StringBuilder(locker
                        +" is holding the"+lockType+" lock '"+name+
                        "' for more than "+maxWait+
                        " ms.\nLocker stack trace:\n");

                for (StackTraceElement element : stackElements) {
                        msg.append("\t");
                        msg.append(element);
                        msg.append("\n");
                }

                log.error(msg.toString());
        }
}

When it runs, it will print an error to our logging system (you
can use any logging system you want). I wish we could do more, but
there is no way we can actually release the lock from a (different)
timer thread (at least, no way I can think of).

To complete the puzzle, let's look at the
LockTaskStack private inner class:


private class LockTaskStack
        extends ThreadLocal<Stack<WaitTimerTask>> {

        @Override
        protected Stack<WaitTimerTask> initialValue() {
                return new Stack<WaitTimerTask>();
        }
}

Just a simple ThreadLocal extension.

Preventing Write Lock After Read Lock

As mentioned before, we also want to prevent trying to lock for
writing while holding a read lock. Normally this will just block
the locking thread without any notification. We will add a check
inside the setWriteLock() method that checks if the
same thread is already holding a read lock, and if so, will bail
out without trying to lock for writing.

To achieve this, the following check is added to the existing
setWriteLock() method:


Stack<WaitTimerTask> taskStack = lockTaskStack.get();

if (!taskStack.isEmpty()) {
        WaitTimerTask job = taskStack.peek();
        if (job!=null && job.isReadLock()) {
                log.fatal("The same thread ["+Thread.currentThread()+
                "] is already holding a read lock '"+name+
                "'. Cannot lock for write!");
                return;
        }
}

You can add more information to the log, such as the thread's stack
trace. This is done in a similar way to the previous code.

Conclusion

In this article, I have shown how you can extend the wonderful
ReadWriteLock mechanism found in the JDK with
additional fault-preventing features. Although we cannot prevent
all kind of deadlocks in runtime, we can at least get notifications
and details in our log files.

You can take this example and extend it to meet your needs and
hopefully build better multithreaded Java applications.

Resources

width="1" height="1" border="0" alt=" " />
Ran Kornfeld is currently working as an independent Java consultant and instructor, specializing in Java EE, Java SE, and surrounding technologies.
Related Topics >> Programming   |