Objects locking

Synchronized blocks are only one of methods to handle concurrent access to Java's objects. Another one are the implementation of classes included in java.util.concurrent.locks package.

Locks package provides a fresh view to locking management. At the first part of this article, we'll see what are the components of this package. At the second one, through some JUnit cases, we'll see how these components work together.

Concurrent locks in Java

The most basic interface used to manage locks through java.util.concurrent.locks package is Lock. It defines 6 methods:
- lock: as its name indicates, this method tries to acquire a lock on one object. If the object is already locked by another thread, thread demanding the lock passes to disabled state and starts to waiting until the lock becomes again available. Disabled state means that demanding thread doesn't continue to execute its code and is stopped at the lock() invocation.
- lockInterruptibly: behaves almost as lock() method but in additionally, when the thread acquiring the lock is interrupted, InterruptedException is thrown.
- newCondition: creates an instance of java.util.concurrent.locks.Condition interface. An instance of Condition can be used to synchronize the collaboration of two distinct threads trying to acquire the same lock. It works almost as native Thread's methods: wait, notify and notifyAll.
- tryLock and tryLock(time, unit) : both methods do the same, they try to acquire a lock until specified time. For the method with empty signature, if the lock isn't available, a false is returned immediately. In the second case, we can determine the trying timeout. During it, the thread will try to acquire a lock. If its operation fails, false is returned. In both cases, if the locks is correctly acquired, true is returned.
Another difference is that timeouted tryLock throws InterruptedException when the thread trying to acquire a lock is interrupted.
In both methods, tryLock behaves as lock() call and freezes demanding thread execution at the point of lock invocation.
- unlock: releases the lock. It must be called. Otherwise, locked object will stay locked and other threads will wait eternally at lock() or tryLock() points, causing deadlocks.

An interesting type of lock is java.util.concurrent.locks.ReadWriteLock. It's specific because it handles a pair of locks, one reserved to read and the second reserved to write operations. This interface defines only two methods, one for acquire reading lock (readLock) and another one for acquiring writing lock (writeLock). Write lock is an exclusive lock. It means that if one thread acquires a write lock on something, they're no threads that can acquire read locks. It means also that write lock can't be acquired while a read lock was acquired before. More than one write or read lock can be acquired at the same time. So you can lock an object with 2 different write or read locks.

Examples of concurrent locks in Java

After the introduction, we can pass to code samples. All comments are included inside the test code. In the test cases we test if different customers are able to try the same sweater. Note that the sweater, represented as Sweater instance, can be tried only by one customer at given moment. Each test case uses its own Sweater instance:

public class LockingTest {

  @Test
  public void testSimpleLock() {
    Sweater sweater = new Sweater();
    assertTrue("Tested object ("+sweater+") should implement Lock interface but it wasn't", (sweater instanceof Lock));
    
    Customer customer1 = new Customer("Customer#1", sweater);
    Customer customer2 = new Customer("Customer#2", sweater);
    Customer customer3 = new Customer("Customer#3", sweater);
    Customer customer4 = new Customer("Customer#4", sweater);
    
    ThreadPoolExecutor service = new ThreadPoolExecutor(4, 4, 6, TimeUnit.SECONDS, new LinkedBlockingQueue(2));
    service.execute(customer1);
    service.execute(customer2);
    service.execute(customer3);
    service.execute(customer4);
    
    long timeout = System.currentTimeMillis()+5000;
    while (service.getCompletedTaskCount() < 4 && System.currentTimeMillis() < timeout) {
    }
    /*
      * Simple lock() use. After 5 seconds, all 
      * customers should be able to try the 
      * sweater because of lock acquired during 1 second. 
      */
    assertTrue(customer1.getName()+" should be able to try the sweater but he wasn't", customer1.isTried());
    assertTrue(customer2.getName()+" should be able to try the sweater but he wasn't", customer2.isTried());
    assertTrue(customer3.getName()+" should be able to try the sweater but he wasn't", customer3.isTried());
    assertTrue(customer4.getName()+" should be able to try the sweater but he wasn't", customer4.isTried());
    System.out.println("Is terminated !");
  }

  @Test
  public void testTryLock() {
    Sweater sweater = new Sweater();
    assertTrue("Tested object ("+sweater+") should implement Lock interface but it wasn't", (sweater instanceof Lock));
    
    TryingCustomer customer1 = new TryingCustomer("Customer#1", sweater);
    TryingCustomer customer2 = new TryingCustomer("Customer#2", sweater);
    TryingCustomer customer3 = new TryingCustomer("Customer#3", sweater);

    ThreadPoolExecutor service = new ThreadPoolExecutor(3, 3, 6, TimeUnit.SECONDS, new LinkedBlockingQueue(2));
    service.execute(customer1);
    service.execute(customer2);
    service.execute(customer3);

    while (service.getCompletedTaskCount() < 3) {
    }
    int tried = 0;
    int notTried = 0;
    if (customer1.isTried()) {
            tried++;
    } else {
            notTried++;
    }
    if (customer2.isTried()) {
            tried++;
    } else {
            notTried++;
    }
    if (customer3.isTried()) {
            tried++;
    } else {
            notTried++;
    }
    /*
      * Only 2 customers should be able to try the sweater. The last one 
      * doesn't have enough time to do that (try during 2 seconds 
      * while the sleep with acquired lock takes 2 seconds), ie:
      * 1st stage:
      * - Thread1: locks during 2 seconds
      * - Thread2: tries to lock during 2 seconds
      * - Thread3: tries to lock during 2 seconds
      * --------------------------
      * 2nd stage:
      * - Thread1: releases the lock
      * - Thread2 or Thread3: acquires released lock
      * - Thread2 or Thread3 (other than in previous point): 2 seconds 
      * after, this thread can't acquire a lock because
      * another thread did)
      */
    assertTrue("2 customers should try the sweater, but only "+tried+" were", tried == 2);
    assertTrue("1 customer shouldn't be able to try the sweater, but "+notTried+" were", notTried == 1);
    System.out.println("Is terminated !");
  }
  
  @Test
  public void testLockReadWrite() {
    Message lock = new Message();
    Writer writer = new Writer(lock);
    Reader readerEn = new Reader(lock);
    Reader readerDe = new Reader(lock);
    Reader readerPl = new Reader(lock);
    
    writer.write();
    
    ThreadPoolExecutor service = new ThreadPoolExecutor(3, 3, 6, TimeUnit.SECONDS, new LinkedBlockingQueue(2));
    service.execute(readerPl);
    service.execute(readerEn);
    service.execute(readerDe);
    
    assertTrue("Message should be write-locked", lock.isWriteLocked());
    assertTrue("Message should be write-locked by the current thread", lock.isWriteLockedByCurrentThread());
    assertTrue("They should be no read locks acquired (write lock is hold and read lock can't be acquired)", lock.getReadLockCount() == 0);
    
    // small sleep; otherwise getQueueLength sees only 1 reader
    try {
            Thread.sleep(50);
    } catch(Exception e) {
            e.printStackTrace();
    }
    int waitingQueueLength = lock.getQueueLength();
    assertTrue("3 threads should be waiting for acquire read lock, but only "+waitingQueueLength+" were", waitingQueueLength == 3);

    long timeout = System.currentTimeMillis()+5000;
    while (service.getCompletedTaskCount() < 3 && System.currentTimeMillis() < timeout) {
    }
    // now, release write lock and check if all 3 waiting threads can acquire it
    writer.endWrite();
    assertFalse("Message shouldn't be write-locked", lock.isWriteLocked());

    // Sleeps 2 seconds because read locks remain in "acquired" state during 3 seconds (if we put more than 2 seconds to sleep, read locks will be unlocked inside finally block)
    try {
            Thread.sleep(2000);
    } catch(Exception e) {
            e.printStackTrace();
    }
    assertTrue("Current thread shouldn't hold any read lock, but it was ("+lock.getReadHoldCount()+" read locks hold)", lock.getReadHoldCount() == 0);
    assertTrue("They should be 3 read locks acquired (read locks aren't exlusive locks), but only "+lock.getReadLockCount()+" were", lock.getReadLockCount() == 3);
    waitingQueueLength = lock.getQueueLength();
    assertTrue("0 threads should be waiting for acquire read lock, but "+waitingQueueLength+" are still waiting", waitingQueueLength == 0);
  }
  
  @Test
  public void readWriteNextTest() {
    Message message = new Message();
    
    // Acquire read lock and try to acquired directly write lock. It should be impossible.
    Lock read = message.readLock();
    boolean wasReadLocked = read.tryLock();
    assertTrue("Read lock should be acquired but it wasn't", wasReadLocked);

    Lock write = message.writeLock();
    boolean wasWriteLocked = write.tryLock();
    assertFalse("Write lock shouldn't be acquired when read lock is acquired, but it was", wasWriteLocked);
    
    // Release read lock and try to acquire 3 write locks
    read.unlock();
    assertTrue("Read locks number should be equal to 0 but was "+message.getReadLockCount(), message.getReadLockCount() == 0);

    wasWriteLocked = write.tryLock();
    assertTrue("Write lock should be acquired, but it wasn't", wasWriteLocked);
    
    Lock newWrite = message.writeLock();
    boolean wasNewWriteLocked = newWrite.tryLock();
    assertTrue("The second write lock can't be acquired when the one write lock is already acquired", wasNewWriteLocked);

    Lock thirdWrite = message.writeLock();
    boolean wasThirdWriteLocked = thirdWrite.tryLock();
    assertTrue("The third write lock can't be acquired when the one write lock is already acquired", wasThirdWriteLocked);

    // Try to acquire read lock when 3 write locks remain acquired on the shared object
    wasReadLocked = read.tryLock();
    assertTrue("Read lock should be acquired when write lock is acquired, but it wasn't", wasReadLocked);

    // Unlock all
    write.unlock();
    newWrite.unlock();
    thirdWrite.unlock();
    read.unlock();
  }
  
  @Test
  public void testInterrupted() {
    Sweater sweater = new Sweater();
    assertTrue("Tested object ("+sweater+") should implement Lock interface but it wasn't", (sweater instanceof Lock));
    
    Thread thread1 = new Thread(new InterruptedCustomer("Customer#1", sweater));
    Thread thread2 = new Thread(new Customer("Customer#2", sweater));
    Thread thread3 = new Thread(new InterruptedCustomer("Customer#3", sweater));

    thread1.start();
    thread2.start();
    thread3.start();
    
    /*
      * Both calls will provoke InterruptedException, but 
      * both exception will be applied to different elements:
      * 
      * - this exception corresponds to thread2.interrupt(). 
      *   thread2 wraps Customer instance which acquires 
      *   a lock through standard lock() method
      * 
      * java.lang.InterruptedException: sleep interrupted
      * at java.lang.Thread.sleep(Native Method)
      * at com.mysite.test.concurrency.Customer.run(LockingTest.java:355)
      * at java.lang.Thread.run(Unknown Source)
      * 
* * - this is an exception of thread3 which, through lockInterruptibly(), * tries to acquire a lock on Sweater object *
      * java.lang.InterruptedException
      * at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(Unknown Source)
      * at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(Unknown Source)
      * at com.mysite.test.concurrency.InterruptedCustomer.run(LockingTest.java:276)
      * at java.lang.Thread.run(Unknown Source)
      * 
* */ thread2.interrupt(); thread3.interrupt(); try { Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } } } class Message extends ReentrantReadWriteLock { private String title; public void setTitle(String title) { this.title = title; } } class Writer { private Message lock; private Lock writeLock; public Writer(Message lock) { this.lock = lock; this.writeLock = this.lock.writeLock(); } public void write() { this.writeLock.lock(); System.out.println("Write lock acquired"); try { this.lock.setTitle("XXXXX"); Thread.sleep(2000); } catch (Exception e) { e.printStackTrace(); } } public void endWrite() { this.writeLock.unlock(); } } class Reader implements Runnable { private Message lock; public Reader(Message lock) { this.lock = lock; } @Override public void run() { System.out.println("Reader is trying to acquire a read lock"); Lock readLock = this.lock.readLock(); readLock.lock(); System.out.println("Read lock acquired"); try { Thread.sleep(3000); } catch (Exception e) { e.printStackTrace(); } finally { readLock.unlock(); } } } class Sweater extends ReentrantLock { private static final long serialVersionUID = 2153107313764490981L; @Override public String toString() { return "Sweater"; } } abstract class CustomerAbstract { protected Sweater locked; protected boolean tried; protected String name; public CustomerAbstract(String name, Sweater locked) { this.name = name; this.locked = locked; } public String getName() { return this.name; } public boolean isTried() { return this.tried; } } class InterruptedCustomer extends CustomerAbstract implements Runnable { public InterruptedCustomer(String name, Sweater locked) { super(name, locked); } @Override public void run() { try { this.locked.lockInterruptibly(); Thread.sleep(5000); System.out.println(">"+this.name+" is trying"); this.tried = true; } catch (Exception e) { e.printStackTrace(); } finally { if (this.tried) { this.locked.unlock(); } } } } class TryingCustomer extends CustomerAbstract implements Runnable { public TryingCustomer(String name, Sweater locked) { super(name, locked); } @Override public void run() { try { if (this.locked.tryLock(2, TimeUnit.SECONDS)) { Thread.sleep(2000); System.out.println(">"+this.name+": I'm trying "+locked); this.tried = true; } else { System.out.println(">"+this.name+": I can't try this sweater"); } } catch (Exception e) { e.printStackTrace(); } finally { if (this.tried) { this.locked.unlock(); } } } } class Customer extends CustomerAbstract implements Runnable { public Customer(String name, Sweater locked) { super(name, locked); } public String getName() { return this.name; } @Override public void run() { try { this.locked.lock(); // If the lock is already acquired, this Thread sleeps until the lock is released Thread.sleep(1000); System.out.println(">"+this.name+": I'm trying "+locked); this.tried = true; } catch (Exception e) { e.printStackTrace(); } finally { this.locked.unlock(); } }
>Customer#1: I'm trying Sweater
>Customer#2: I'm trying Sweater
>Customer#3: I'm trying Sweater
>Customer#4: I'm trying Sweater
Is terminated !
>Customer#1: I'm trying Sweater
>Customer#2: I can't try this sweater
>Customer#3: I'm trying Sweater
Is terminated !
Write lock acquired
Reader is trying to acquire a read lock
Reader is trying to acquire a read lock
Reader is trying to acquire a read lock
Read lock acquired
Read lock acquired
Read lock acquired
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(Unknown Source)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(Unknown Source)
	at com.mysite.test.concurrency.InterruptedCustomer.run(LockingTest.java:329)
	at java.lang.Thread.run(Unknown Source)
>Customer#1 is trying
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.mysite.test.concurrency.Customer.run(LockingTest.java:387)
	at java.lang.Thread.run(Unknown Source)

In previous examples we saw that Lock is generalized version of synchronized blocks. Thanks to it, we can reserve the access to one object only for one method. We can also use a more sophisticated locking mechanism, and control object reading only when it's not writing (ReadWriteLock).

If you liked it, you should read:

The comments are moderated. I publish them when I answer, so don't worry if you don't see yours immediately :)

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!