Explicit locks in Java 5 (ctd)

Lock basics

The basic locking paradigm is as follows: we have some Lock object, which we explicitly call a lock method on. When we're finished (i.e. in a finally clause), we explicitly unlock it:

Lock lck = ...

lck.lock();
try {
   ... access shared resource
} finally {
  lck.unlock();
}

As mentioned above, Lock is an interface. The implementation provided is ReentrantLock1. As an example, we'll use a ReentrantLock to implement a simple concurrent integer array. (This is just an example of using locks: in reality, AtomicIntegerArray provides a better performing alternative.)

public class ConcurrentIntegerArray {
  private final Lock lock = new ReentrantLock();
  private final int[] arr;

  public ConcurrentIntegerArray(int size) {
    arr = new int[size];
  }
  public void set(int index, int value) {
    lock.lock();
    try {
      arr[index] = value;
    } finally {
      lock.unlock();
    }
  }
  public int get(int index) {
    lock.lock();
    try {
      return arr[index];
    } finally {
      lock.unlock();
    }
  }
}

In this example, we have an underlying array, arr, and we wrap accesses to this array in a lock/unlock pair. Once thing you may be wondering is about memory access semantics. With regular synchronized blocks, the JVM guarantees that entering and exiting the synchronized block acts as a "memory barrier", meaning that, for example, where a memory write occurs inside the synchronized block, this write cannot be re-ordered to occur after the block is exited (else another thread might not see it). Here, there's no synchronized, so how does it work? Well, it turns out that a contract of the Lock interface is that it provides the same memory barrier behaviour as synchronized. What that basically means is that we can safely use the lock/unlock action as the thing that guarnatees that a write by Thread A will be seen by Thread B.

In case it's not obvious: once we've chosen a Lock as our method of synchronizing access to a particular variable, we should make sure we use only that same lock object for all accesses. In particular, we can't mix synchronization via a Lock with synchronization via synchronized and expect it to work! I mention this mainly because it's an accident that could happen if you are adapting legacy code to use explicit locks instead of synchronized blocks2. (When constructing the object, it is OK to rely on final here to make sure that the arr reference set by one thread is visible to other threads accessing the get/set methods.)


Notes:
1. A lock is said to be re-entrant if the owning thread can call its lock method multiple times without blocking. To release the lock, the owning thread must call the unlock method as many times as it called the lock method.
2. Of course, if you've used good encapsulation, all of the locking needed by a particular class will be internal to that class and it'll be easy to find all the places where synchronized is used, won't it...?


If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.

Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.