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 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

Ran Kornfeld is currently working as an independent Java consultant and instructor, specializing in Java EE, Java SE, and surrounding technologies.
Related Topics >> Programming   |