Skip to content

Java Multithreading locking strategies

#POST4

There are several locking strategies that can be used to synchronize access to shared resources between multiple threads.

Additionally, the java.util.concurrent.atomic package provides a set of classes for working with atomic variables such as AtomicInteger and AtomicReference, which can improve performance in scenarios where there is a high level of contention for the lock.

Below are some of the most common locking strategies:

Synchronized blocks and methods:

This is the most common locking strategy in Java. It involves using the synchronized keyword to ensure that only one thread at a time can access a shared resource. You can synchronize a block of code or a method by adding the synchronized keyword before the block or method declaration.

public class Counter {
    private int count;
    
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

In this example, the increment() and getCount() methods are synchronized, which ensures that only one thread at a time can modify or read the count variable.Example of a synchronized.

public class MessageQueue {
    private List<Message> messages = new ArrayList<>();
    
    public void addMessage(Message message) {
        synchronized(messages) {
            messages.add(message);
        }
    }
    
    public Message getMessage() {
        synchronized(messages) {
            if(messages.isEmpty()) {
                return null;
            } else {
                return messages.remove(0);
            }
        }
    }
}

In this example, the addMessage() and getMessage() methods synchronize on the messages list, which ensures that only one thread at a time can modify or read from the list.

ReentrantLock:

This is a more flexible alternative to synchronized blocks and methods. The ReentrantLock class allows you to acquire and release locks on a shared resource in a more fine-grained way than synchronized blocks and methods. It also provides additional features such as timed locks and interruptible locks.

public class Counter {
    private int count;
    private ReentrantLock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

In this example, the increment() and getCount() methods use a ReentrantLock to ensure that only one thread at a time can modify or read the count variable.

ReadWriteLock:

This is a specialized lock that allows multiple threads to read a shared resource concurrently, but only allows one thread to write to the resource at a time. The ReadWriteLock class provides two locks: a read lock and a write lock.

public class MessageQueue {
    private List<Message> messages = new ArrayList<>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void addMessage(Message message) {
        lock.writeLock().lock();
        try {
            messages.add(message);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public Message getMessage() {
        lock.readLock().lock();
        try {
            if(messages.isEmpty()) {
                return null;
            } else {
                return messages.remove(0);
            }
        } finally {
            lock.readLock().unlock();
        }
    }
}

In this example, the addMessage() method uses a write lock and the getMessage() method uses a read lock to ensure that multiple threads can read the messages list concurrently, but only one thread can modify the list at a time.

StampedLock:

This is a newer type of lock introduced in Java 8 that provides an optimistic locking strategy for read-heavy workloads. It allows multiple threads to read a shared resource concurrently, but requires a write lock to be acquired before any thread can modify the resource.

public class Counter {
    private int count;
    private StampedLock lock = new StampedLock();
    
    public void increment() {
        long stamp = lock.writeLock();
        try {
            count++;
        } finally {
            lock.unlockWrite(stamp);
        }
    }
    
    public int getCount() {
        long stamp = lock.tryOptimisticRead();
        int c = count;
        if(!lock.validate(stamp)) {
            stamp = lock.readLock();
            try {
                c = count;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return c;
    }
}

In this example, the increment() method uses a write lock to ensure that only one thread at a time can modify the count variable, while the getCount() method uses an optimistic read lock to allow multiple threads to read the variable concurrently.

The main benefit of using a StampedLock over other ways of locking is that it provides a more flexible and efficient way to synchronize access to shared resources, especially for read-heavy workloads.

Another advantage of a StampedLock is that it supports both exclusive write locks and optimistic read locks, as well as a variety of other lock modes. This makes it more flexible than other locking strategies, which often only support exclusive locks.

However, it’s important to note that StampedLock is not always the best choice for every scenario. For example, if the shared resource is frequently modified, or if contention is high, then a ReentrantLock or ReadWriteLock might be a better choice. Also, the use of optimistic locking with a StampedLock can result in increased complexity and potential errors, so it requires careful consideration and testing before implementation.

Atomic variables:

Atomic variables are thread-safe variables that provide atomic operations such as compare-and-set and get-and-set. They can be used as a locking strategy when dealing with numerical values or simple flags.

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();
    }
    
    public int getCount() {
        return count.get();
    }
}


AtomicReference

import java.util.concurrent.atomic.AtomicReference;

public class Example {
    private AtomicReference<String> value = new AtomicReference<>("initial value");
    
    public void updateValue(String oldValue, String newValue) {
        boolean updated = value.compareAndSet(oldValue, newValue);
        if (updated) {
            System.out.println("Value updated successfully!");
        } else {
            System.out.println("Value update failed. Current value: " + value.get());
        }
    }
    
    public String getValue() {
        return value.get();
    }
}

The Example class uses an AtomicReference to store a String value. The updateValue() method uses the compareAndSet() method of the AtomicReference class to atomically compare the current value of the reference with the oldValue parameter, and if they match, set the new value to newValue. The method returns true if the value was updated, and false otherwise.

Using compareAndSet() ensures that the value is updated atomically and safely, without the need for explicit locking or synchronization. It also allows for checking whether the update was successful or not, which can be useful in scenarios where multiple threads might be trying to update the value concurrently.

Note that the compareAndSet() method only updates the value if the current value matches the expected oldValue parameter. If the current value has changed since the oldValue was obtained, the update will fail and the method will return false. In this case, the current value can be obtained using the get() method of the AtomicReference.

Conclusion

Choosing the right locking strategy depends on the specific requirements of your application.

In general, synchronized blocks and methods are the simplest and most widely used locking strategy in Java, but the other locking strategies may be more appropriate for specific use cases.

1 thought on “Java Multithreading locking strategies”

  1. Pingback: How to handle Java Multithreading by writing thread-safe programs? - Concurrent Core

Leave a Reply

Your email address will not be published. Required fields are marked *

Top Time Complexities Top Java String Tricky interview questions 7 steps to build programming logic