Mastering Multithreading in Java: Part 5 – ReentrantLock and Volatile

Mastering Multithreading in Java: Part 5 – ReentrantLock and Volatile

Recap of Synchronization and Mutex

In the previous article, we discussed the basics of synchronization in Java. We explored how the synchronized keyword helps us prevent race conditions by allowing only one thread to access a critical section of code at a time. We also introduced the concept of a mutex (mutual exclusion), which helps achieve the same goal but with more control over the locking and unlocking of resources.

Today, we’ll take a closer look at two more essential tools in Java’s multithreading toolkit: ReentrantLock and volatile. These concepts will further help you manage thread interactions, avoid errors, and write efficient multithreaded programs.


What is ReentrantLock?

The ReentrantLock is a more flexible and powerful alternative to the synchronized keyword. While synchronized gives you automatic locking and unlocking (once a thread finishes executing a synchronized block, the lock is released), the ReentrantLock gives you more control.

A ReentrantLock allows you to:

  1. Lock and unlock sections of code manually.
  2. Try locking a section of code without blocking.
  3. Implement timeouts when acquiring locks.
  4. Use conditions to signal threads (more advanced, but extremely useful).

Let’s first understand the basics of how ReentrantLock works with an example.


Basic Example of ReentrantLock

class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // Acquire the lock
        try {
            count++;
        } finally {
            lock.unlock(); 
        }
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final count: " + counter.getCount());
    }
}        

How ReentrantLock Works

In the above example, the ReentrantLock is used to control access to the increment() method, which updates a shared variable (count). The lock.lock() method is called before modifying the shared resource, ensuring that only one thread can execute that block of code at a time. The lock.unlock() method releases the lock when the thread is finished, allowing other threads to proceed.

Key Points:

  • The lock must always be unlocked after it has been used. That’s why we place the unlock() method inside a finally block—this ensures the lock is always released, even if an exception is thrown.
  • You can use ReentrantLock when you need more control over locking and unlocking than synchronized offers.


Advanced Features of ReentrantLock

While the basic usage of ReentrantLock is straightforward, it also provides more advanced features like tryLock() and lockInterruptibly().

tryLock() – Non-blocking Lock

The tryLock() method allows a thread to attempt to acquire a lock without waiting indefinitely. If the lock is not available, the method returns false, and the thread can decide what to do next.

if (lock.tryLock()) {

    try {
        // Critical section
    } finally {
        lock.unlock();
    }
} else {
    // Do something else
}        

lockInterruptibly() – Responding to Interrupts

Sometimes, you might want a thread to be interruptible while waiting for a lock. The lockInterruptibly() method allows a thread to be interrupted if it’s waiting to acquire a lock.

lock.lockInterruptibly();        

What is Volatile?

In Java, volatile is another mechanism to handle multithreaded access to shared variables. It’s a keyword you can apply to variables that ensures that all threads see the most recent value of the variable. Normally, without volatile, each thread might cache a variable’s value, meaning changes made by one thread may not be immediately visible to others.


Why Use Volatile?

The volatile keyword guarantees:

  1. Visibility: Changes made by one thread are immediately visible to all other threads.
  2. No Reordering: Operations on volatile variables are not reordered by the compiler or JVM. This prevents some tricky synchronization issues.

Here’s an example of using volatile:

class VolatileExample {
    private volatile boolean running = true;

    public void stopRunning() {
        running = false;
    }

    public void doWork() {
        while (running) {
            // Do some work
        }
        System.out.println("Thread stopped.");
    }
}

public class Main {

    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        Thread workerThread = new Thread(example::doWork);
        workerThread.start();
        Thread.sleep(1000);
        example.stopRunning(); 
        workerThread.join();
        System.out.println("Main thread finished.");
    }
}        

In this example, the running variable is marked as volatile, meaning any change to it by one thread will be visible to the other thread. Without volatile, the worker thread might not see the change in running, and it could continue looping indefinitely.


When to Use Volatile?

The volatile keyword is ideal when you only need to guarantee visibility of changes to a variable across threads. It’s simpler than using locks, but it has limitations:

  • It doesn’t guarantee atomicity. If you need to perform compound actions (like x++), you still need to use locks or atomic variables.
  • Use volatile for flags, status variables, or any situation where one thread updates a variable and others just read it.


ReentrantLock vs. Volatile

While both ReentrantLock and volatile help with thread safety, they serve different purposes:

  • ReentrantLock is used when you need to manage access to a critical section of code. It ensures that only one thread can execute a particular block of code at a time.
  • Volatile is used to ensure visibility of a variable across multiple threads, so changes made by one thread are immediately visible to others.


Conclusion: Mastering ReentrantLock and Volatile

In this article, we covered two powerful tools for managing multithreading in Java: ReentrantLock and volatile. ReentrantLock provides fine-grained control over locking and unlocking, while volatile ensures that variables are consistently updated across threads. These tools are essential for building thread-safe programs and avoiding common pitfalls like race conditions and inconsistent data.

As you continue your journey in Java multithreading, remember to use ReentrantLock when you need more control than synchronized offers, and volatile when you need to ensure visibility of shared variables.

In the next article, we’ll dive into more advanced synchronization techniques, such as deadlock prevention and thread-safe collections. Stay tuned for more insights on mastering multithreading in Java!


Previously Covered Topics in This Series:

要查看或添加评论,请登录

Allan Crowley的更多文章

社区洞察

其他会员也浏览了