Search code examples
javamultithreadingconcurrencyreentrantlock

Reentrantlock - Why do we need to acquire a lock multiple times?


I have recently been learning multithreading concepts in java. I have a few doubts which didn't resolve by looking up the relevant threads on StackOverflow. I couldn't find satisfactory answers for my following questions:

  1. wait() method makes the thread to wait till it gets the lock. Whereas, the wait(long timeout) method makes the thread wait for 'timeout' no. of milliseconds and if it still doesn't get the lock, goes back to runnable state. But to actually get to running state, it needs the lock however. So what's the point of wait(long timeout) method? The thread however releases the locks acquired by it when it's in waiting state. So the difference is not even the resources acquired by it. What difference does it make if the thread stays in waiting state or runnable state? What's the advantage of wait(long timeout) over wait() method?

  2. synchonized keyword or block provides lock on the object on which the method or block is called. It causes another thread which tries to acquire the lock on the same instance to wait. But in the case of ReentrantLock, on which object is the lock acquired? The threads trying to acquire whose lock are made to wait?

  3. How does a ReentrantLock avoid deadlock? Suppose there are two methods m1 and m2. Both need to acquire a lock. m1 is calling m2 and m2 is calling m1. How can we avoid deadlock in this situation using ReentrantLock? May be we can use tryLock() and provide an alternate operations for the thread which fails to acquire the lock. But what could be the possible alternate operations? What if the thread must need the lock to work?

  4. I found that using ReentrantLock we can acquire lock multiple times. But why do we have to acquire lock several times? I have read theoretical answers on this but couldn't really get it. It will be helpful if you can demonstrate with a clear example code.


Solution

  • Why do we need to acquire a lock multiple times?

    Clearly, you don't need to. But it is not unusual for an application to do it "by accident". For example:

      public void binaryOperation(Operand op1, Operand op2) {
          synchronized (op1) {
              synchronized (op2) {
                   // do something that needs the locks
              }
          }
      }
    
      // now call passing the same object for both operands
      Operand op = ...
      binaryOperation(op, op); 
    

    In this example, the op object will actually get locked twice. If primitive locks weren't re-entrant, this could would fail (or deadlock).

    Now we could fix the binaryOperation method to not do that, but it would make the code significantly more complicated.

    The same scenario can happen with ReentrantLock.


    Question 1.

    But to actually get to running state, it needs the lock however. So what's the point of wait(long timeout) method?

    This is about Object::wait. The ReentrantLock API doesn't support this. (Beware: you can use wait and notify on a ReentrantLock object, but only if you are treating it as a primitive lock. Not a good idea!)

    The wait is waiting for a notification, and the timeout says how long the caller is prepared to wait for the notification. As the javadoc says:

    "Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed."

    With both wait() and wait(timeout), it is up to the caller to check that the condition that it was expecting to be "notified about" has actually satisfied. (See the note about "spurious wakeup" ... and the example code.)

    What's the advantage of wait(long timeout) over wait() method?

    Simply, it gives you the option of only waiting a limited time for the notification. If this is not useful, don't use it.


    Question 2.

    But in the case of ReentrantLock, on which object is the lock acquired?

    Strictly speaking, it is the lock itself. What that the lock actually means will depend on how you code your classes. But that is exactly the same as with primitive mutexes.

    Locking in Java doesn't prevent some misbehaving code from accessing and or updating some shared state without holding the lock. It is up to the programmer to do it correctly.

    The threads trying to acquire whose lock are made to wait?

    Yes.


    Question 3.

    How does a ReentrantLock avoid deadlock?

    In general, it doesn't.

    In the case of reentrant locking (i.e. in the case where a thread attempts to acquire lock A while holding lock A), the ReentrantLock implementation notices that the thread holding the lock is the thread acquiring the lock. A counter is incremented, so that the implementation knows that the lock must be released twice.

    How can we avoid deadlock in this situation using ReentrantLock? May be we can use tryLock() and provide an alternate operations for the thread which fails to acquire the lock.

    That is one approach.

    But what could be the possible alternate operations?

    1. Ensure that all threads acquire the locks in the same order. (Deadlocks occur when threads attempt to acquire two or more threads in a different order.)

    2. If tryLock fails while holding a different lock, release the lock, wait a bit, and try again.

    What if the thread must need the lock to work?

    Then you design the logic so that deadlock is avoided; see the alternatives above!


    Question 4.

    But why do we have to acquire lock several times?

    As stated above, you typically don't. But the point of a ReentrantLock is that you don't have to worry in the case where you do end up acquiring the lock twice ... for whatever reason.