Locking Mechanisms in Java

Locking Mechanisms in Java

https://medium.com/hprog99/locking-mechanism-in-java-c23142d4707b

Locking is a mechanism in Java that allows a thread to exclusively acquire a lock on an object or a class, preventing other threads from accessing the locked object or class until the lock is released. In this blog, we will discuss the different types of locks in Java and provide examples to demonstrate how they can be used in a multi-threaded environment.

Object Locks

Object locks are used to synchronize access to an object’s methods and fields. When a thread acquires a lock on an object, no other thread can execute any of the object’s synchronized methods until the lock is released. To acquire a lock on an object, a thread can use the synchronized keyword when declaring a method or use the synchronized block.

class MyClass {
    public synchronized void method1() {
        // code here
    }

    public void method2() {
        synchronized(this) {
            // code here
        }
    }
}

In the above example, method1 is declared as synchronized, which means that only one thread can execute this method at a time. method2 uses a synchronized block to acquire a lock on the current object, which prevents other threads from executing this block of code until the lock is released.

Class Locks

Class locks are used to synchronize access to a class’s static methods and fields. When a thread acquires a lock on a class, no other thread can execute any of the class’s synchronized static methods until the lock is released. To acquire a lock on a class, a thread can use the synchronized keyword when declaring a static method or use the synchronized block.

class MyClass {
    public static synchronized void method1() {
        // code here
    }

    public static void method2() {
        synchronized(MyClass.class) {
            // code here
        }
    }
}

In the above example, method1 is declared as synchronized, which means that only one thread can execute this method at a time. method2 uses a synchronized block to acquire a lock on the class, which prevents other threads from executing this block of code until the lock is released.

Reentrant Locks

Reentrant locks are a more advanced form of locks that provide more flexibility and control over the locking mechanism. A reentrant lock allows a thread to acquire a lock multiple times and release it multiple times, while a regular lock can only be acquired and released once.

class MyClass {
    private final ReentrantLock lock = new ReentrantLock();

    public void method1() {
        lock.lock();
        try {
            // code here
        } finally {
            lock.unlock();
        }
    }

    public void method2() {
        if (lock.tryLock()) {
            try {
                // code here
            } finally {
                lock.unlock();
            }
        } else {
            // code here
        }
    }
}

In the above example, method1 uses the lock() method to acquire a lock and the unlock() method to release it. method2 uses the tryLock() method to try to acquire a lock and returns a boolean indicating whether the lock was acquired or not.

Read-Write Locks

Read-write locks are a specialized form of locks that are used to control access to a shared resource. They allow multiple threads to read the shared resource simultaneously, but only one thread can write to it at a time. This can greatly improve the performance of a multi-threaded application when the shared resource is read more frequently than it is written.

class MyClass {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public void method1() {
        lock.readLock().lock();
        try {
            // code here
        } finally {
            lock.readLock().unlock();
        }
    }

    public void method2() {
        lock.writeLock().lock();
        try {
            // code here
        } finally {
            lock.writeLock().unlock();
        }
    }
}

In the above example, method1 uses the readLock() method to acquire a read lock and the unlock() method to release it. method2 uses the writeLock() method to acquire a write lock and the unlock() method to release it.

Stamped Locks

Stamped locks were introduced in Java 8 as an alternative to read-write locks, they offer more fine-grained control over read and write operations, as well as a try-optimistic-locking mechanism.

A stamped lock works by associating a stamp with a lock state, the stamp serves as a token that verifies the lock state when performing operations. The stamped lock provides three modes of operation:

Optimistic Read

This mode allows multiple threads to read the shared resource simultaneously without acquiring a lock. The stamp returned by the tryOptimisticRead() method is used to validate the lock state before reading the shared resource. If the stamp is valid, the thread can proceed with reading the shared resource. If the stamp is invalid, it means that the lock state has been changed and the thread must acquire a read lock to access the shared resource.

StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();

if (lock.validate(stamp)) {
    // read shared resource
} else {
    stamp = lock.readLock();
    try {
        // read shared resource
    } finally {
        lock.unlockRead(stamp);
    }
}

Read

This mode allows only one thread to read the shared resource at a time. The readLock() method is used to acquire a read lock and the stamp returned by the method is used to release the lock.

StampedLock lock = new StampedLock();
long stamp = lock.readLock();
try {
    // read shared resource
} finally {
    lock.unlockRead(stamp);
}

Write

This mode allows only one thread to write to the shared resource at a time. The writeLock() method is used to acquire a write lock and the stamp returned by the method is used to release the lock.

StampedLock lock = new StampedLock();
long stamp = lock.writeLock();
try {
    // write to shared resource
} finally {
    lock.unlockWrite(stamp);
}

In addition to these basic modes, stamped locks also provide other methods such as tryWriteLock() and tryConvertToWriteLock() that can be used to perform more advanced operations such as upgrading a read lock to a write lock and more.

Semaphores

Semaphores are a more advanced form of locks that can be used to control access to a shared resource. They allow multiple threads to acquire the lock simultaneously, but limit the number of threads that can acquire the lock at any given time. This is useful in scenarios where there are limited resources available, such as a pool of connections or a cache.

class MyClass {
    private final Semaphore semaphore = new Semaphore(5);

    public void method1() {
        semaphore.acquire();
        try {
            // code here
        } finally {
            semaphore.release();
        }
    }
}

In the above example, method1 uses the acquire() method to acquire a permit from the semaphore and the release() method to release it. The semaphore is initialized with a maximum of 5 permits, which means that at most 5 threads can acquire the lock at the same time.

CountDownLatch

A CountDownLatch is a synchronization aid that allows one or more threads to wait for a set of operations to complete. It is initialized with a count, and the count is decremented each time a thread completes an operation. When the count reaches zero, all threads waiting on the latch are released.

class MyClass {
    private final CountDownLatch latch = new CountDownLatch(3);

    public void method1() {
        // code here
        latch.countDown();
    }

    public void method2() {
        latch.await();
        // code here
    }
}

In the above example, method1 decrements the count of the latch and method2 uses the await() method to wait for the count to reach zero before executing the code.

In conclusion, Locking is a fundamental mechanism in multi-threaded programming and Java provides a rich set of tools for controlling access to shared resources. The different types of locks such as object locks, class locks, reentrant locks, read-write locks, stamped locks, semaphores, and countdownlatch provide different levels of control and flexibility over the locking mechanism, allowing developers to choose the appropriate lock for their specific use case. It’s important to choose the right lock type and use it properly to avoid synchronization issues such as deadlocks and livelocks.