Welcome!

Java Authors: Maureen O'Gara, Liz McMillan, Walter H. Pinson, III, Yakov Werde, Tony Bishop

Related Topics: Java

Java: Article

Java Feature — Concurrent Programming and Locking in J2SE 5.0

The mechanics of using the Lock interface implementations

The only way to avoid deadlock is by preventing the occurrence of one of the three conditions of policy above (which is often beyond the developer's control) or by preventing the occurrence of a circular wait. The usual way of preventing a circular wait is to design a lock hierarchy and make sure that all threads acquire their locks in the same sequence.

As always, developers are solely responsible for preventing deadlock in their applications, but some of the new utilities in J2SE 5.0 can help with this. For example, a thread will block indefinitely when it tries to acquire a lock that's owned by another thread. An alternative is the lockInterruptibly() method from the Lock interface. If the interrupt() method is called on a thread while it's waiting to acquire a lock, an InterruptedException is thrown and the attempt to acquire the lock is abandoned. From here, one possible course of action would be to throw a fresh exception on the basis of which the program can decide what to do next in the absence of a lock. For example:

Lock lock = new ReentrantLock();
try {
    lock.lockInterruptibly();
    try {
       // Lock acquired, so work with the object state as normal
    } finally {lock.unlock(); }
    } catch (InterruptedException e) {
       // Can't acquire the lock. So end, or throw a new exception to let the caller know.
    }

Rather than trying to acquire a lock, possibly waiting, and possibly needing to be interrupted, lock acquisition can also be more tentative:

    Lock lock = new ReentrantLock();
if (lock.tryLock())
       // Lock acquired, so work with the object state as normal
       try { . . . }
       finally { lock.unlock(); }
else
    // Can't acquire the lock, so take another path

The tryLock() method tries to acquire a lock and will return true if it was successful, otherwise it immediately returns false. If a thread can't acquire a lock, some alternative execution path can be taken. This alternative path may use a physical resource or data structure that isn't quite as fast or efficient as the first choice, but at least the program is progressing rather than spinning its wheels.

The tryLock() method can also be called with a timeout parameter. For example:

if (lock.tryLock(100, TimeUnit.MILLISECONDS))...

Using tryLock() with a timeout parameter has other advantages. If tryLock() is called with a timeout parameter, an InterruptedException will be thrown if the thread is interrupted during the timeout period. The exception handling in this case might include releasing any resources held and trying some alternative execution path, making the chances of a deadlock occurring less likely.

The same sort of timeouts can also be specified when waiting on conditions.

condition.await(100, TimeUnit.MILLISECONDS))

In common with tryLock(), the await() method returns if the timeout has elapsed or if the thread is interrupted, and in the normal cases where another thread calls the signalAll() or signal() methods.

Lock testing and timeouts give developers more options for keeping their applications out of deadlock, but this comes at a small cost. Preventing circular waits using a lock hierarchy simply means working out the order in which threads should acquire their locks. Meanwhile, to use lock testing and timeouts an application must be designed to allow alternative execution paths or ways of cleaning up its state so another attempt can be made later. Even so, the power of lock testing and timeouts lies in providing an application with more than one way of getting a task done rather than relying on the anti-deadlock smarts of a single-path algorithm.

Performance
It's most often true that acquiring a contended lock causes a bigger performance hit than acquiring an uncontended lock. One way to avoid this performance hit is to actively manage an application's concurrency by making sure as little work as possible is done under locking. The goal should be to obtain a lock, do whatever is necessary, and then release the lock quickly. If some time-consuming task has to be done, it should be moved out of the locked area wherever possible.

Another way to improve an application's concurrency is to use a ReentrantReadWriteLock. Rather than creating a blanket mutually exclusive lock, as in the case of the ReentrantLock, the ReentrantReadWriteLock defines reading and writing locks. A write lock is still mutually exclusive, but if none of these is active, then a read lock can be held by more than one reader thread at once.

The ReentrantReadWriteLock works best in situations where there will be more reader threads than writer threads as when working with a Lightweight Directory Access Protocol (LDAP)-like data structure. Whether the ReentrantReadWriteLock delivers any noticeable performance benefits really depends on this imbalance between reader and writer threads being maintained. Still, it's another tool that developers can call on.

Which Method to Use, Old or New?
Even though the locking utilities in J2SE 5.0 do the same as the existing concurrency primitives and much more, they aren't automatic first-choice replacements. So, at some point the question will arise of which to use. This will naturally depend on the nature of the application being built, but there are some rules of thumb to help decide between the two.

First, avoid using both if at all possible. Unmistakeably, concurrent applications are more complex to design, code, and debug than sequential applications. Keeping an application as simple as necessary could mean avoiding all flavours of handcrafted concurrency. Still, there may be functional alternatives in the existing thread-safe collections, synchronization wrappers, or some of the new concurrent collections. This pushes the responsibility for thread management onto tried -and tested fundamental Java classes.

But if an application's design calls for a choice to be made, using the synchronized keyword produces code that is simpler and more concise. For example, the single synchronized keyword acquires an object's lock and ensures that it's released when the method or block exits, whether by normal means or by exception. Meanwhile, the new utilities rely on the idiom of explicitly acquiring a lock and then releasing it inside a finally block; it's the developer's responsibility to get this right.

Using synchronized can also have advantages when debugging applications. Because lock acquisition and release is handled by the JVM, the JVM is able to provide locking information when generating thread dumps. Meanwhile, the Lock implementations are vanilla Java classes and the same level of debugging detail isn't available.

Still, the features of the J2SE 5.0 locking utilities are alluring. Multiple condition variables, timed lock waits, interruptible lock waits, and broader lock scope offer developers great power. Doug Lea led the JSR 166 (Concurrency Utilities) specification team and the bulk of the new utilities come from his util.concurrent package, which has been around since the late 1990s. This means the utilities have been peer-reviewed and stress-tested over time to deliver the best possible performance and scalability.

So, when choosing between the old and new exclusion techniques, the best advice would be: start out with the synchronized keyword until it proves to be inadequate, then move onto the new utilities when the need for the extra features or performance is justified.

References

More Stories By Craig Caulfield

Craig Caulfield is a senior software engineer for a defense and commercial software house in Perth, Western Australia. He has a Bachelors degree in Computer Science, a Masters degree in Software Engineering, and holds certifications in Java, XML, DB2, UML, MySQL, and WebSphere.

Comments (2) View Comments

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.


Most Recent Comments
SYS-CON Brazil News Desk 04/21/06 01:50:09 PM EDT

In concurrent programming, exclusion refers to any technique that dynamically locks certain blocks of code so multiple threads can't corrupt their shared resources in ways that can cause integrity problems. In Java, exclusion has meant using the synchronized keyword against a method or block of code to control access to an object's lock.

SYS-CON India News Desk 04/21/06 01:03:31 PM EDT

In concurrent programming, exclusion refers to any technique that dynamically locks certain blocks of code so multiple threads can't corrupt their shared resources in ways that can cause integrity problems. In Java, exclusion has meant using the synchronized keyword against a method or block of code to control access to an object's lock.