Deep understanding of java built in lock of synchronized and explicit lock of ReentrantLock

  • 2020-11-26 18:47:11
  • OfStack

synchronized and Reentrantlock

In multithreaded programming, we use locks when our code needs to be synchronized. Java provides us with both a built-in lock (synchronized) and an explicit lock (ReentrantLock) for synchronization. Explicit locks are introduced by JDK1.5. What are the similarities and differences between these two types of locks? Is there just one more option or is there something else? Find out for yourself in this article.


// synchronized Keyword usage example 
public synchronized void add(int t){//  Synchronized methods 
  this.v += t;
}

public static synchronized void sub(int t){//  Synchronous static method 
  value -= t;
}
public int decrementAndGet(){
  synchronized(obj){//  Synchronous code block 
    return --v;
  }
}

That's all there is to the built-in lock, you've learned.

The built-in lock is very convenient to use, no need to explicitly acquire and release, any 1 object can be used as a built-in lock. Using built-in locks can solve most synchronization scenarios. "Any 1 object can be used as a built-in lock" also means that every time the synchronized keyword appears, there is an object associated with it, specifically:

When synchronized is applied to a normal method, the lock object is this;

When synchronized is applied to a static method, the lock object is an Class object of the current class;

When synchronized ACTS on a block of code, the lock object is this obj in synchronized(obj).

The explicit lock

Why do you need an extra explicit lock when the built-in lock works so well? Because there are some things you can't do with a built-in lock, like:

We want to add a waiting time timeout to the lock, and we give it up before we get the lock, so we don't have to wait indefinitely;

We want to get the lock in an interruptible way so that an external thread can send us an interrupt signal and awaken the thread waiting for the lock.

We want to maintain multiple wait queues for locks, such as 1 producer queue, 1 consumer queue, and 1 side to improve lock efficiency.

Explicit locking (ReentrantLock) was formally created to address these flexible requirements. ReentrantLock literally means reentrant lock, and reentrant means that a thread can request the same lock multiple times at the same time without causing its own deadlock. Here's the difference between a built-in lock and an explicit lock:

Timed: ES53en.tryLock (long timeout, TimeUnit unit) provides a way to end a wait with a timed end, which returns false and ends the thread wait if the thread has not acquired the lock within the specified time.

Interruptible: As you've definitely seen with InterruptedException, many multithreaded methods throw this exception, which is not a burden caused by a defect, but a necessity, or a good thing. Interruptability gives us a way to prematurely end a thread (rather than having to wait for the thread to finish executing), which is useful for canceling time-consuming tasks. With the built-in lock, the thread waits until the built-in lock is reached, and there is no other way for it to end the wait than by acquiring the lock. RenentrantLock. lockInterruptibly() gives us a way to end a wait with an interrupt.

Conditional queue (condition queue) : After acquiring a lock, a thread may enter a wait state because it is waiting for a condition to occur (the built-in lock is via the Object.wait () method, and the explicit lock is via the Condition.await () method). The thread entering the wait state will suspend and automatically release the lock, and these threads will be placed into the conditional queue. synchronized corresponds to only one condition queue, while ReentrantLock can have multiple condition queues. What is the benefit of multiple queues? Please look down.

Conditional predicates: After a thread acquires a lock, it sometimes has to wait for a condition to be satisfied, such as a producer waiting for "cache dissatisfaction" to put a message into the queue, and a consumer waiting for "cache non-empty" to get a message out of the queue. These conditions are called conditional predicates, and the thread needs to first acquire the lock, then determine if the conditional predicate is satisfied, and if it is not satisfied, it does not proceed, and the corresponding thread abandons execution and automatically releases the lock. Different threads using the same lock may have different condition predicates. If there is only one condition queue, it is impossible to determine which thread in the condition queue should be awakened when a certain condition predicate is satisfied. But if each condition predicate has a separate condition queue, we know that the thread on the corresponding queue should be woken when a condition is met (the built-in lock is woken by the Object.notify () or Object.notifyAll () methods, and the explicit lock is woken by the Condition.signal () or Condition.signalAll () methods). That's the advantage of having multiple conditional queues.

With a built-in lock, the object itself is both a lock and a condition queue. When using an explicit lock, the object of RenentrantLock is a lock, and the condition queue is obtained by the RenentrantLock.newCondition () method, which can be called multiple times to obtain multiple condition queues.

A typical example of using an explicit lock is as follows:


//  An example of the use of explicit locks 
ReentrantLock lock = new ReentrantLock();

//  Get the lock, this is the heel synchronized Keywords corresponding usage. 
lock.lock();
try{
  // your code
}finally{
  lock.unlock();
}

//  Can be timed, beyond the specified time to get the lock on the abandon 
try {
  lock.tryLock(10, TimeUnit.SECONDS);
  try {
    // your code
  }finally {
    lock.unlock();
  }
} catch (InterruptedException e1) {
  // exception handling
}

//  Interruptible, thread threads can be interrupted while waiting for the lock to be acquired 
try {
  lock.lockInterruptibly();
  try {
    // your code
  }finally {
    lock.unlock();
  }
} catch (InterruptedException e) {
  // exception handling
}

//  Multiple waiting queues for specific reference [ArrayBlockingQueue](https://github.com/CarpenterLee/JCRecipes/blob/master/markdown/ArrayBlockingQueue.md)
/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();

Note that the above code places unlock() in the finally block, which is required. The explicit lock does not automatically release as the built-in lock does. With explicit lock 1, you must manually release the lock in the finally block. If the lock is not released after acquisition for an abnormal reason, the lock will never be released! Place unlock() in the finally block to ensure that it will be released normally no matter what happens.

conclusion

Built-in locks can solve most scenarios that require synchronization, and explicit locks should only be considered if additional flexibility is required, such as features such as timing, interruptability, and multi-wait queues.

Explicit locks are flexible, but they need to be explicitly requested and released, and the release 1 must be placed in the finally block, otherwise the lock may never be released due to an exception! This is the most obvious disadvantage of explicit locking.

In summary, when synchronization is required, give priority to the more secure and easier to use implicit locks.


Related articles: