Concurrent Programming – [ Locks API ]

In Java, apart from using synchronized keyword to make thread safe of a shared resource, there are other ways, using locks api is one of them.

A lock is a more flexible and sophisticated thread synchronization mechanism than the standard block marked by synchronized keyword.

What You Need

  • About 14 minutes
  • A favorite text editor or IDE
  • Java 8 or later

1. Lock Interface

The Lock interface is defined inside java.util.concurrent.lock package, and it provides extensive operations for locking.

  • void lock() : acquire the lock if it’s available. If the lock isn’t available, a thread gets blocked until the lock is released;
  • void lockInterruptibly() : this is similar to the lock(), but it allows the blocked thread to be interrupted and resume the execution through a thrown java.lang.InterruptedException;
  • boolean tryLock() : this is a non-blocking version of lock() method. It attempts to acquire the lock immediately, return true if locking succeeds;
  • boolean tryLock(long timeout, TimeUnit timeUnit) : This is similar to tryLock(), except it waits up the given timeout before giving up trying to acquire the Lock;
  • void unlock() : unlocks the Lock instance;
  • newCondition() : the Condition class provides the ability for a thread to wait for some condition to occur while executing the critical section.
    This can occur when a thread acquires the access to the critical section but doesn’t have the necessary condition to perform its operation.
    For example, a reader thread can get access to the lock of a shared queue that still doesn’t have any data to consume.
    Traditionally Java provides wait(), notify() and notifyAll() methods for thread intercommunication.
    Conditions have similar mechanisms.

A locked instance should always be unlocked to avoid deadlock condition.

A recommended code block to use the lock should contain a try/catch and finally block.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockAPI {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            // access to the shared resource
        } finally {
            lock.unlock();
        }
    }
}

2. Lock Implementations

2.1 ReentrantLock

ReentrantLock offers the same concurrency and memory semantics as using synchronized keyword, with extended capabilities.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockAPIReentrantLock {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(10);

        Counter counter = new Counter();

        for (int i = 0; i < 1000; i++) {
            service.submit(counter::inc);
        }

        service.shutdown();

        service.awaitTermination(10, TimeUnit.SECONDS);

        System.out.println("sum = " + counter.sum);
    }

    private static class Counter {
        private int sum = 0;

        private Lock lock = new ReentrantLock();

        void inc() {
            this.lock.lock();
            try {
                this.sum++;
            } finally {
                this.lock.unlock();
            }
        }
    }
}

Below is the output of above code snippet :

sum = 1000

Below is an example of using tryLock() to do the same.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockAPIReentrantLock2 {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(10);

        Counter counter = new Counter();

        for (int i = 0; i < 1000; i++) {
            service.submit(counter::inc);
        }

        service.shutdown();

        service.awaitTermination(10, TimeUnit.SECONDS);

        System.out.println("sum = " + counter.sum);
    }

    private static class Counter {
        private int sum = 0;

        private Lock lock = new ReentrantLock();

        void inc() {
            while (true) {
                if (this.lock.tryLock()) {
                    try {
                        this.sum++;
                        return;
                    } finally {
                        this.lock.unlock();
                    }
                }
            }
        }
    }
}

Below is the output of above code snippet :

sum = 1000

Below is an example of using wait(), notify() and notifyAll() methods to ensure the communication of multi-threads on a shared resource.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class LockAPIReentrantLock3 {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(10);

        Container container = new Container(5);

        for (int i = 0; i < 10; i++) {
            Integer element = i;
            service.submit(() -> container.push(element));
        }

        for (int i = 0; i < 9; i++) {
            service.submit(container::pop);
        }

        service.shutdown();

        service.awaitTermination(10, TimeUnit.SECONDS);

        container.display();
    }

    private static class Container {
        private int capacity;
        private List<Integer> list;

        Container(int capacity) {
            this.capacity = capacity;
            this.list = new ArrayList<>(capacity);
        }

        void push(int element) {
            synchronized (this.list) {
                while (this.list.size() >= this.capacity) {
                    try {
                        this.list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                try {
                    System.out.println(Thread.currentThread().getName() + " push " + element);
                    this.list.add(element);
                } finally {
                    this.list.notifyAll();
                }
            }
        }

        int pop() {
            synchronized (this.list) {
                while (this.list.size() <= 0) {
                    try {
                        this.list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                try {
                    System.out.println(Thread.currentThread().getName() + " pop");
                    return this.list.remove(this.list.size() - 1);
                } finally {
                    this.list.notifyAll();
                }
            }

        }

        void display() {
            for (int element : this.list) {
                System.out.print(element);
                System.out.print(" ");
            }
            System.out.println();
        }
    }
}

Below is the output of above code snippet :

pool-1-thread-1 push 0
pool-1-thread-10 push 9
pool-1-thread-10 pop
pool-1-thread-9 push 8
pool-1-thread-9 pop
pool-1-thread-8 push 7
pool-1-thread-7 push 6
pool-1-thread-7 pop
pool-1-thread-7 pop
pool-1-thread-7 pop
pool-1-thread-6 push 5
pool-1-thread-4 push 3
pool-1-thread-5 push 4
pool-1-thread-3 push 2
pool-1-thread-2 push 1
pool-1-thread-9 pop
pool-1-thread-8 pop
pool-1-thread-10 pop
pool-1-thread-1 pop
5

Below is an example of using conditions of lock for the same.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockAPIReentrantLock4 {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(10);

        Container container = new Container(5);

        for (int i = 0; i < 10; i++) {
            Integer element = i;
            service.submit(() -> container.push(element));
        }

        for (int i = 0; i < 9; i++) {
            service.submit(container::pop);
        }

        service.shutdown();

        service.awaitTermination(10, TimeUnit.SECONDS);

        container.display();
    }

    private static class Container {
        private int capacity;
        private List<Integer> list;
        private Lock lock;
        private Condition empty;
        private Condition full;

        Container(int capacity) {
            this.capacity = capacity;
            this.list = new ArrayList<>(capacity);
            this.lock = new ReentrantLock();
            this.empty = this.lock.newCondition();
            this.full = this.lock.newCondition();
        }

        void push(int element) {
            this.lock.lock();

            while (this.list.size() >= this.capacity) {
                try {
                    this.full.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            try {
                System.out.println(Thread.currentThread().getName() + " push " + element);
                this.list.add(element);
            } finally {
                this.empty.signalAll();
                this.lock.unlock();
            }
        }

        int pop() {
            this.lock.lock();

            while (this.list.size() <= 0) {
                try {
                    this.empty.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            try {
                System.out.println(Thread.currentThread().getName() + " pop");
                return this.list.remove(this.list.size() - 1);
            } finally {
                this.full.signalAll();
                this.lock.unlock();
            }

        }

        void display() {
            for (int element : this.list) {
                System.out.print(element);
                System.out.print(" ");
            }
            System.out.println();
        }
    }
}

The output of above code snippet is below :

pool-1-thread-1 push 0
pool-1-thread-2 push 1
pool-1-thread-3 push 2
pool-1-thread-4 push 3
pool-1-thread-5 push 4
pool-1-thread-1 pop
pool-1-thread-1 pop
pool-1-thread-9 push 8
pool-1-thread-9 pop
pool-1-thread-9 pop
pool-1-thread-10 push 9
pool-1-thread-4 pop
pool-1-thread-3 pop
pool-1-thread-2 pop
pool-1-thread-6 push 5
pool-1-thread-7 push 6
pool-1-thread-8 push 7
pool-1-thread-1 pop
pool-1-thread-5 pop
5

2.2 ReentrantReadWriteLock

In addition to the Lock interface, there is a ReadWriteLock interface that maintains a pair of locks, one for read-only operations and one for the write operations.

  • Lock readLock() : if no thread acquired the write lock or requested for it, multiple threads can acquire the read lock;
  • Lock writeLock() : if no threads are reading or writing, only one thread can acquire the write lock.

In below example, as we can noticed that some times the output of one reader thread cross another one’s, while the output of every writer thread is not crossed at all.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockAPIReentrantReadWriteLock {
    public static void main(String[] args) throws InterruptedException {
        Container container = new Container();

        Runnable read = () -> {
            container.read();
        };

        Runnable writeEven = () -> {
            for (int i = 0; i < 10; i++) {
                if (i % 2 == 0) {
                    container.write(i);
                }
            }
        };

        Runnable writeOdd = () -> {
            for (int i = 0; i < 10; i++) {
                if (i % 2 != 0) {
                    container.write(i);
                }
            }
        };

        Thread reader1 = new Thread(read);
        Thread reader2 = new Thread(read);
        Thread reader3 = new Thread(read);
        Thread reader4 = new Thread(read);
        Thread reader5 = new Thread(read);
        Thread writer1 = new Thread(writeOdd);
        Thread writer2 = new Thread(writeEven);

        writer1.start();
        writer2.start();

        TimeUnit.MILLISECONDS.sleep(100);

        reader1.start();
        reader2.start();
        reader3.start();
        reader4.start();
        reader5.start();

        writer1.join();
        writer2.join();
        reader1.join();
        reader2.join();
        reader3.join();
        reader4.join();
        reader5.join();
    }

    private static class Container {
        private List<Integer> list;
        private ReadWriteLock lock;

        Container() {
            this.list = new ArrayList<>();
            this.lock = new ReentrantReadWriteLock();
        }

        void write(int element) {
            try {
                this.lock.writeLock().lock();
                System.out.println(Thread.currentThread().getName() + " write :");
                System.out.println(element);
                this.list.add(element);
            } finally {
                this.lock.writeLock().unlock();
            }
        }

        void read() {
            try {
                this.lock.readLock().lock();
                System.out.println(Thread.currentThread().getName() + " read :");
                System.out.println(Arrays.toString(this.list.toArray()));
            } finally {
                this.lock.readLock().unlock();
            }
        }
    }
}

Below is the output of above code snippet :

Thread-6 write :
0
Thread-6 write :
2
Thread-6 write :
4
Thread-6 write :
6
Thread-6 write :
8
Thread-5 write :
1
Thread-5 write :
3
Thread-5 write :
5
Thread-5 write :
7
Thread-5 write :
9
Thread-0 read :
[0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
Thread-1 read :
Thread-3 read :
Thread-2 read :
[0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
[0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
[0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
Thread-4 read :
[0, 2, 4, 6, 8, 1, 3, 5, 7, 9]

2.3 StampedLock

StampedLock is an alternative of using a ReadWriteLock.

Like ReentrantReadWriteLock, if we have readers than writers, using a StampedLock can significantly improve performance.

The main differences between StampedLock and ReentrantReadWriteLock are that :

  • ReentrantReadWriteLock is reentrant, while StampedLock is not;
  • StampedLock allows optimistic locking for read operations.

Below is using StampedLock to do the same example of ReentrantReadWriteLock.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class LockAPIStampedLock {
    public static void main(String[] args) throws InterruptedException {
        Container container = new Container();

        Runnable read = () -> {
            container.read();
        };

        Runnable writeEven = () -> {
            for (int i = 0; i < 10; i++) {
                if (i % 2 == 0) {
                    container.write(i);
                }
            }
        };

        Runnable writeOdd = () -> {
            for (int i = 0; i < 10; i++) {
                if (i % 2 != 0) {
                    container.write(i);
                }
            }
        };

        Thread reader1 = new Thread(read);
        Thread reader2 = new Thread(read);
        Thread reader3 = new Thread(read);
        Thread reader4 = new Thread(read);
        Thread reader5 = new Thread(read);
        Thread writer1 = new Thread(writeOdd);
        Thread writer2 = new Thread(writeEven);

        writer1.start();
        writer2.start();

        TimeUnit.MILLISECONDS.sleep(100);

        reader1.start();
        reader2.start();
        reader3.start();
        reader4.start();
        reader5.start();

        writer1.join();
        writer2.join();
        reader1.join();
        reader2.join();
        reader3.join();
        reader4.join();
        reader5.join();
    }

    private static class Container {
        private List<Integer> list;
        private StampedLock lock;

        Container() {
            this.list = new ArrayList<>();
            this.lock = new StampedLock();
        }

        void write(int element) {
            long stamp = this.lock.writeLock();
            try {
                System.out.println(Thread.currentThread().getName() + " write :");
                System.out.println(element);
                this.list.add(element);
            } finally {
                this.lock.unlockWrite(stamp);
            }
        }

        void read() {
            long stamp = this.lock.readLock();
            try {
                System.out.println(Thread.currentThread().getName() + " read :");
                System.out.println(Arrays.toString(this.list.toArray()));
            } finally {
                this.lock.unlockRead(stamp);
            }
        }
    }
}

The output of above code snippet is below :

Thread-5 write :
1
Thread-5 write :
3
Thread-5 write :
5
Thread-5 write :
7
Thread-5 write :
9
Thread-6 write :
0
Thread-6 write :
2
Thread-6 write :
4
Thread-6 write :
6
Thread-6 write :
8
Thread-0 read :
Thread-1 read :
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
Thread-2 read :
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
Thread-3 read :
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
Thread-4 read :
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]

Another feature provided by StampedLock is optimistic locking.

Sometimes, read operations don’t need to wait for write operation completion, and as a result of this, the exclusive read lock isn’t required.

It is a situation when you acquired the write lock and written something and you wanted to read in the same critical section and to not break the potential concurrent access.

Method ‘public long tryOptimisticRead()’ acquires a non-exclusive lock without blocking only when it returns a stamp that can be later validated.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.StampedLock;

public class LockAPIStampedLock2 {
    public static void main(String[] args) throws InterruptedException {
        Container container = new Container();

        Runnable read = () -> {
            container.read();
        };

        Runnable writeEven = () -> {
            for (int i = 0; i < 10; i++) {
                if (i % 2 == 0) {
                    container.write(i);
                }
            }
        };

        Runnable writeOdd = () -> {
            for (int i = 0; i < 10; i++) {
                if (i % 2 != 0) {
                    container.write(i);
                }
            }
        };

        Thread reader1 = new Thread(read);
        Thread reader2 = new Thread(read);
        Thread reader3 = new Thread(read);
        Thread reader4 = new Thread(read);
        Thread reader5 = new Thread(read);
        Thread writer1 = new Thread(writeOdd);
        Thread writer2 = new Thread(writeEven);

        writer1.start();
        writer2.start();

        reader1.start();
        reader2.start();
        reader3.start();
        reader4.start();
        reader5.start();

        writer1.join();
        writer2.join();
        reader1.join();
        reader2.join();
        reader3.join();
        reader4.join();
        reader5.join();
    }

    private static class Container {
        private List<Integer> list;
        private StampedLock lock;

        Container() {
            this.list = new ArrayList<>();
            this.lock = new StampedLock();
        }

        void write(int element) {
            long stamp = this.lock.writeLock();
            try {
                System.out.println(Thread.currentThread().getName() + " write :");
                System.out.println(element);
                this.list.add(element);
            } finally {
                this.lock.unlockWrite(stamp);
            }
        }

        void read() {
            long stamp = this.lock.tryOptimisticRead();

            if (!lock.validate(stamp)) {
                stamp = lock.readLock();
                try {
                    System.out.println(Thread.currentThread().getName() + " read :");
                    System.out.println(Arrays.toString(this.list.toArray()));
                } finally {
                    this.lock.unlockRead(stamp);
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " optimistic read :");
                System.out.println(Arrays.toString(this.list.toArray()));
            }
        }
    }
}

Below is the output of above code snippet :

Thread-5 write :
1
Thread-5 write :
3
Thread-5 write :
5
Thread-5 write :
7
Thread-5 write :
9
Thread-6 write :
0
Thread-6 write :
2
Thread-6 write :
4
Thread-6 write :
6
Thread-6 write :
8
Thread-4 read :
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
Thread-3 read :
Thread-0 read :
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
Thread-2 read :
Thread-1 read :
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]
[1, 3, 5, 7, 9, 0, 2, 4, 6, 8]

3. LockSupport

The LockSupport class is usually used for creating locks and synchronization classes, it can’t be instantiated and offers only static methods for use.

The two methods offered by this class are park() and unpark() along with their variations.

These methods provide low-level alternatives to the deprecated methods Thread.suspend() and Thread.resume().

The LockSupport class associates a single permit with each thread that uses it.

If the permit is not available, then thread invoking park() is blocked.

The blocked thread can subsequently be unblocked using the unpark() method by passing-in the blocked thread object as argument.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

public class LockAPILockSupport {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("Thread 1 will be parked");
            LockSupport.park("t1's blocker");
            System.out.println("Thread 1 after being parked");
        });

        t1.start();

        Thread t2 = new Thread(() -> {
            System.out.println("Thread 2 will be parked");
            LockSupport.park("t2's blocker");
            System.out.println("Thread 2 after being parked");
        });

        t2.start();

        TimeUnit.SECONDS.sleep(1);

        Object blocker1 = LockSupport.getBlocker(t1);

        System.out.println("Blocker 1 is " + blocker1);

        Object blocker2 = LockSupport.getBlocker(t2);

        System.out.println("Blocker 2 is " + blocker2);

        LockSupport.unpark(t1);
        LockSupport.unpark(t2);
    }
}

Below is the output of above code snippet :

Thread 1 will be parked
Thread 2 will be parked
Blocker 1 is t1's blocker
Blocker 2 is t2's blocker
Thread 1 after being parked
Thread 2 after being parked

If the permit is available, the thread invoking park() doesn’t block.

Consider the snippet below, where the main thread unparks, makes the permit available, then parks and consumes the permit and moves forward.

If the order of the two operations in the snippet is reversed, the main thread will be permanently blocked.

import java.util.concurrent.locks.LockSupport;

public class LockAPILockSupport2 {
    public static void main(String[] args) throws InterruptedException {
        // get a permit
        LockSupport.unpark(Thread.currentThread());
        // consume a permit
        LockSupport.park();
        System.out.println("Main thread exiting");
    }
}

/*
 Output :
    Main thread exiting
 */

The output of above code snippet is below :

Main thread exiting