Deep Understanding of ReentrantLock: Unlocking the Mysteries of Java Concurrent Programming

  sonic0002        2023-05-22 08:01:13       4,156        0    

ReentrantLock introduction

ReentrantLock is a class in the Java concurrent package, java.util.concurrent.locks, and is an implementation of the Lock interface. As its name suggests, it is a reentrant mutual exclusion lock.

A mutual exclusion lock is a synchronization tool used to protect shared resources, ensuring that only one thread can access the resource at a given time. Reentrant means that a thread can acquire the same lock multiple times without causing a deadlock.

This lock provides some basic behaviors that are the same as those of the built-in synchronization mechanism. For example, a thread holding the lock can enter any section of code protected by the lock, while a thread that does not hold the lock cannot enter and must wait for the thread holding the lock to release it before entering.

But compared to the built-in synchronization mechanism, ReentrantLock also provides some more advanced features:

1. Interruptible lock acquisition: When a thread is waiting to acquire a lock, it can be interrupted.

2. Fairness: The lock can be set to fair, meaning the thread that has been waiting for the longest time will be given priority to acquire the lock.

3. Lock surrender: When a thread is waiting to acquire a lock, it can give up waiting and do other things.

4. Multiple condition variables: The built-in lock only has one condition variable (each object's built-in lock is associated with a condition queue), while ReentrantLock can have one or more condition variables. This allows for more fine-grained control of thread waiting.

Although ReentrantLock is more powerful, it is a lower-level mechanism and is more complicated to use than the built-in synchronization mechanism. Compared to using the synchronized keyword, ReentrantLock offers greater flexibility in handling locks. Unless the advanced features provided by ReentrantLock are needed, it is generally recommended to use the built-in synchronization mechanism.

Frequently used methods

1. ReentrantLock(): This is the default constructor of ReentrantLock, which creates an instance of ReentrantLock. The lock is non-fair by default, meaning that if multiple threads are waiting for the lock, any of them could potentially acquire it when it is released.

2. ReentrantLock(boolean fair): This is another constructor of ReentrantLock, which creates an instance of ReentrantLock and determines whether the lock is fair based on the boolean value passed in. If fair is true, the thread that has been waiting the longest will acquire the lock when it is released.

3. void lock(): This method is used to acquire the lock. If the lock is already held by another thread, the current thread will be blocked until the lock is released.

4. void unlock(): This method is used to release the lock. If the current thread holds the lock, the hold count of the lock will be decremented. If the hold count becomes 0, the lock will be released. If the current thread does not hold the lock, calling this method will result in an IllegalMonitorStateException.

5. boolean tryLock(): This method attempts to acquire the lock. If the lock is not held by another thread, it acquires the lock and immediately returns true. If the lock is already held by another thread, it does not acquire the lock and immediately returns false.

6. boolean tryLock(long timeout, TimeUnit unit): This method attempts to acquire the lock within the giventime period. If the lock becomes available during this time and the current thread is not interrupted, it acquires the lock and returns true. Otherwise, it returns false.

7. void lockInterruptibly(): This method is similar to lock(), but if the current thread is interrupted while waiting for the lock, it will throw an InterruptedException.

8. boolean isHeldByCurrentThread(): Returns true if the current thread holds this lock.

9. int getHoldCount(): Returns the hold count of this lock for the current thread, which is the number of times lock() has been called by the current thread.

These are some of the main methods of the ReentrantLock class, providing fine-grained control over lock behavior, including interruptible lock acquisition, try-locking, and more.

Examples

Here is an example of using ReentrantLock, which simulates a thread-safe counter:

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

public class Counter {
    private final ReentrantLock lock = new ReentrantLock(); // 1.create ReentrantLock insatnce
    private int count = 0;

    public void increment() {
        lock.lock(); // 3.acquire lock
        try {
            count++;
            System.out.println("Current thread [" + Thread.currentThread().getName() + "] holds the lock, count = " + count
                + ", hold count = " + lock.getHoldCount()); // 9.check current lock count for current thread
        } finally {
            lock.unlock(); // 4.release lock
        }
    }

    public void tryIncrement() {
        if (lock.tryLock()) { // 5.tries to acquire lock
            try {
                count++;
                System.out.println("Current thread [" + Thread.currentThread().getName() + "] holds the lock, count = " + count);
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Current thread [" + Thread.currentThread().getName() + "] failed to acquire the lock");
        }
    }

    public void tryIncrementWithTimeout() {
        try {
            if (lock.tryLock(1, TimeUnit.SECONDS)) { // 6.tries to acquire lock within specified time
                try {
                    count++;
                    System.out.println("Current thread [" + Thread.currentThread().getName() + "] holds the lock, count = " + count);
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println("Current thread [" + Thread.currentThread().getName() + "] failed to acquire the lock");
            }
        } catch (InterruptedException e) {
            System.out.println("Current thread [" + Thread.currentThread().getName() + "] is interrupted");
        }
    }

    public void incrementInterruptibly() {
        try {
            lock.lockInterruptibly(); // 7.acquire lock with interruption
            try {
                count++;
                System.out.println("Current thread [" + Thread.currentThread().getName() + "] holds the lock, count = " + count);
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            System.out.println("Current thread [" + Thread.currentThread().getName() + "] is interrupted");
        }
    }

    public void printCount() {
        System.out.println("Current count = " + count);
    }
}

This Counter class provides four different methods for incrementing the count, each of which corresponds to a different usage of ReentrantLock. Multiple threads can be created and these methods can be called to observe their behavior.

Note that when using ReentrantLock, it is important to release the lock in the finally block to ensure that it is always released.

newCondition()

newCondition() is an important method in the ReentrantLock class, used to create a Condition instance associated with the current ReentrantLock object.

In ReentrantLock, you can create one or more Condition objects. Each Condition object has a wait queue for storing threads that have called Condition.await().

Here are some key points about the newCondition() method in ReentrantLock:

Condition newCondition(): This method creates a new Condition instance associated with the current ReentrantLock instance. The return value of this method is a Condition object.

The Condition interface is an interface in Java concurrent programming that provides a mechanism for a thread to pause and wait for a condition change. This is similar to the wait() and notify() methods in the Object class, but Condition provides more powerful and flexible thread scheduling capabilities.

Here are some of the main methods of the Condition interface:

1. void await(): Causes the current thread to wait until it is awakened, usually by the signal() or signalAll() method.

2. void signal(): Wakes up one thread waiting on the Condition. If multiple threads are waiting, the choice of which thread to wake up is arbitrary and implementation-dependent.

3. void signalAll(): Wakes up all threads waiting on the Condition.

These methods allow us to more finely control the scheduling of multiple threads. For example, we can wake up one or all threads in the wait queue, or have the current thread wait for a certain amount of time or until a certain condition is met.

Here is an example of using ReentrantLock and Condition to simulate a simple bounded buffer:

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

public class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    private final Object[] items = new Object[100];
    private int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

In this example, the put() method is used to add elements to the cache, and the take() method is used to retrieve elements from the cache. If the cache is full, the put() method will wait until the cache is not full, and if the cache is empty, the take() method will wait until the cache is not empty. When an element is added to or removed from the cache, threads that may be waiting are notified.

By using ReentrantLock and Condition, this example provides more flexible thread control than using the built-in synchronized keyword and wait/notify methods.

Why called ReentrantLock?

The ReentrantLock class in Java is a reentrant mutual exclusion lock, and its name comes from its design: when a thread requests a lock held by another thread, the requesting thread will be blocked; when a thread requests a lock held by itself, the request will succeed and the lock's counter will be incremented, which is called reentrancy.

Here is an example of using ReentrantLock to implement reentrancy:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock();
        try {
            System.out.println("outer lock acquired");
            inner();
        } finally {
            lock.unlock();
            System.out.println("outer lock released");
        }
    }

    public void inner() {
        lock.lock();
        try {
            System.out.println("inner lock acquired");
        } finally {
            lock.unlock();
            System.out.println("inner lock released");
        }
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.outer();
    }
}

In this example, both the outer() and inner() methods attempt to acquire the same lock. When we call the outer() method in the main() method, the lock is first acquired, and then the inner() method is called within outer(). Since the lock is reentrant, the inner() method can successfully acquire the lock without being blocked.

When you run this program, you will see the following output:

outer lock acquired
inner lock acquired
inner lock released
outer lock released

This indicates that the lock was successfully acquired and released in the inner() method, and then released in the outer() method. This is the reentrancy of ReentrantLock.

Lock hold count

"The number of lock holders" or "lock hold count" is an important concept regarding reentrant locks. For a reentrant lock, the same thread can acquire the lock multiple times without being blocked.

For example, if a thread already holds a lock and then tries to acquire the same lock again, if the lock is reentrant, the operation will succeed, and the thread can continue to execute, with the lock hold count incremented by 1. If the lock is not reentrant, the thread will be blocked because it is trying to acquire a lock it already holds.

When a thread completes a code block protected by a lock, it needs to call unlock() to release the lock. Each time unlock() is called, the lock hold count is decremented by 1. Only when the lock hold count becomes 0 is the lock truly released, and other threads have the chance to acquire the lock.

This design allows a thread to acquire and release the same lock multiple times without causing a deadlock. This is very useful in practical programming, for example, when a method calls another method, and both methods require the same lock.

Performance comparison between synchronized and ReentrantLock

Synchronized and ReentrantLock are both tools in Java used to implement thread synchronization, but they have differences in implementation and performance.

1. Implementation

Synchronized is a keyword in Java and is implemented by the JVM. Therefore, after Java code is compiled, the code block of synchronized is converted into special instructions to achieve the effect of locking.

ReentrantLock is a class in the JDK implemented by Java code. It provides more features than synchronized, such as interruptible lock acquisition, fair locks, multiple condition variables, etc.

2. Performance

In earlier versions of the JDK (such as before 1.5), ReentrantLock usually had better performance than synchronized because synchronized was simpler to implement. For example, it cannot respond to interrupts, set timeout time, or implement fair locks. ReentrantLock provides richer synchronization mechanisms through the Lock interface in the java.util.concurrent package.

However, starting from JDK 1.6, Java has made a lot of optimizations to synchronized, such as introducing biased locking, lightweight locking, lock elimination, and lock coarsening, which makes synchronized's performance excellent in most cases, and even in some cases, it may be better than ReentrantLock.

3. Conclusion

Overall, in terms of performance, the difference between synchronized and ReentrantLock is not decisive, and they have their advantages in different situations. The specific choice of which method to use needs to be considered based on the specific situation:

-If finer-grained control is needed, such as interruptible lock acquisition, fair locks, multiple condition variables, or more complex synchronization structures, then ReentrantLock is a better choice.

- If only simple synchronization mechanisms are needed, then using synchronized is more convenient and the code is easier to read.

Finally, it is important to note that regardless of which synchronization tool is used, it is necessary to ensure that the lock is correctly released, otherwise it may cause serious problems such as deadlock or resource leakage.

JAVA  MULTITHREADING  CONCURRENCY  REENTRANTLOCK 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

First law of software quality