What is the difference between synchronized and ReentrantLock?

Preface

Software concurrency has become a basic capability of modern software development, and Java's carefully designed efficient concurrency mechanism is one of the foundations for building large-scale applications.

The focus of this blog post is, what is the difference between synchronized and ReentrantLock? Some people say that synchronized is the slowest. Is this true?  

Frequently asked questions

synchronizedIt is Java's built-in synchronization mechanism, so some people call it Intrinsic Locking. It provides mutually exclusive semantics and visibility. When a thread has acquired the current lock, other threads trying to acquire can only wait or block there.

Before Java 5, synchronized was the only synchronization method. In the code, synchronized can be used to modify methods, or it can be used on specific code blocks. In essence, the synchronized method is equivalent to wrapping all the statements of the method in synchronized blocks. .

ReentrantLock, usually translated as reentrant lock, is the lock implementation provided by Java 5. Its semantics are basically the same as synchronized. The re-entry lock is obtained by directly calling the lock() method through the code, making code writing more flexible. At the same time, ReentrantLock provides many practical methods that can achieve many detailed controls that synchronized cannot achieve, such as controlling fairness, or using defined conditions, etc. However, you also need to pay attention when coding. You must explicitly call the unlock() method to release, otherwise the lock will always be held.

The performance of synchronized and ReentrantLock cannot be generalized. The early version of synchronized has a large performance difference in many scenarios. Many improvements have been made in subsequent versions, and the performance may be better than ReentrantLock in low competition scenarios.  

Specific analysis

Regarding concurrent programming, different companies or interviewers have different interview styles. Some major companies like to keep asking you about the extensions or underlying mechanisms of the relevant mechanisms, and some like to start from a practical perspective, so you need a certain amount of patience in preparing for concurrent programming.

As one of the basic tools of concurrency, locks need to be mastered at least:

  • Understand what thread safety is.
  • Basic uses and cases of synchronized, ReentrantLock and other mechanisms.

To go one step further, you need:

  • Master the underlying implementation of synchronized and ReentrantLock; understand lock expansion and degradation; understand concepts such as skew locks, spin locks, lightweight locks, and heavyweight locks.
  • Master the various implementations and case studies of java.util.concurrent.lock in the concurrency package.  

Practical analysis

First, we need to understand what thread safety is.

In "Java Concurrency in Practice" written by experts such as Brain Goetz, thread safety is a concept of correctness in a multi-threaded environment, that is, ensuring the correctness of shared and modifiable states in a multi-threaded environment . nature, the status here reflected in the program can actually be regarded as data.

From another perspective, if the state is not shared or modifiable, there is no thread safety problem, and two methods to ensure thread safety can be deduced:

  • Encapsulation: Through encapsulation, we can hide and protect the internal state of the object.
  • Immutability: This is true for final and immutable. The Java language currently does not have true native immutability, but it may be introduced in the future.

Thread safety needs to ensure several basic characteristics:

  • Atomicity , simply put, means that related operations will not be interfered with by other threads midway, and is generally achieved through a synchronization mechanism.
  • Visibility means that when a thread modifies a shared variable, its status can be immediately known to other threads. It is usually interpreted as reflecting the thread local status to the main memory. Volatile is responsible for ensuring visibility.
  • Orderliness ensures serial semantics within a thread and avoids instruction rearrangement.

It may be a bit obscure, so let's take a look at the following code snippet and analyze where the atomicity requirement is reflected. This example simulates two operations on shared state by taking two values ​​and comparing them.

You can compile and execute, and you can see that with only low concurrency of two threads, it is very easy to encounter the situation where former and latter are not equal. This is because other threads may have modified sharedState during the two value acquisition processes.

public class ThreadSafeSample {
  public int sharedState;
  public void nonSafeAction() {
      while (sharedState < 100000) {
          int former = sharedState++;
          int latter = sharedState;
          if (former != latter - 1) {
              System.out.printf("Observed data race, former is " +
                      former + ", " + "latter is " + latter);
          }
      }
  }

  public static void main(String[] args) throws InterruptedException {
      ThreadSafeSample sample = new ThreadSafeSample();
      Thread threadA = new Thread(){
          public void run(){
              sample.nonSafeAction();
          }
      };
      Thread threadB = new Thread(){
          public void run(){
              sample.nonSafeAction();
          }
      };
      threadA.start();
      threadB.start();
      threadA.join();
      threadB.join();
  }
}
复制代码

The following are the results of a certain run:

Observed data race, former is 9851, latter is 9853
复制代码

Protect the two assignment processes with synchronized and use this as a mutually exclusive unit to prevent other threads from modifying sharedState concurrently.

synchronized (this) {
  int former = sharedState ++;
  int latter = sharedState;
  // …
}
复制代码

If you use javap to decompile, you can see similar fragments, using the monitorenter/monitorexit pair to achieve synchronization semantics:

11: astore_1
12: monitorenter
13: aload_0
14: dup
15: getfield    #2                // Field sharedState:I
18: dup_x1
…
56: monitorexit
复制代码

Using synchronized in code is very convenient. If it is used to modify a static method, it is equivalent to using the following code to include the method body:

synchronized (ClassName.class) {}
复制代码

Let’s take a look at ReentrantLock. You may be wondering what is reentry? It means that when a thread tries to acquire a lock that it has already acquired, the acquisition action will automatically succeed. This is a concept of lock acquisition granularity, that is, the lock is held in units of threads rather than based on the number of calls. The Java lock implementation emphasizes reentrancy in order to distinguish it from the behavior of pthreads.

Reentrant locks can be set to fairness (fairness), and we can choose whether it is fair when creating a reentrant lock.

ReentrantLock fairLock = new ReentrantLock(true);
复制代码

The so-called fairness here means that in a competition scenario, when fairness is true, the lock will tend to be given to the thread that has been waiting the longest. Fairness is a way to reduce the occurrence of thread "starvation" (in which individual threads wait for a lock for a long time but are never able to obtain it).

If we use synchronized, we cannot make a fair choice at all. It is always unfair. This is also the thread scheduling choice of mainstream operating systems. In general scenarios, fairness may not be as important as imagined, and Java's default scheduling policy rarely causes "starvation" to occur. At the same time, ensuring fairness will introduce additional overhead, which will naturally lead to a certain decrease in throughput. Therefore, I suggest that it is only necessary to specify it if your program truly has a need for fairness.

Let's learn from the perspective of daily coding before entering the lock. To ensure lock release, I recommend that every lock() action immediately corresponds to a try-catch-finally. The typical code structure is as follows. This is a good habit.

ReentrantLock fairLock = new ReentrantLock(true);// 这里是演示创建公平锁,一般情况不需要。
fairLock.lock();
try {
  // do something
} finally {
   fairLock.unlock();
}
复制代码

Compared with synchronized, ReentrantLock can be used like an ordinary object, so you can use the various convenient methods it provides to perform fine synchronization operations, and even implement use cases that are difficult to express with synchronized, such as:

  • Attempt to acquire lock with timeout.
  • You can determine whether there is a thread, or a specific thread, waiting in line to acquire the lock.
  • Can respond to interrupt requests.
  • ...

Here I particularly want to emphasize the condition variable (java.util.concurrent.Condition). If ReentrantLock is an alternative to synchronized, Condition converts wait, notify, notifyAll and other operations into corresponding objects, making complex and obscure synchronization operations Transformed into intuitive and controllable object behavior.

The most typical application scenario of condition variables is ArrayBlockingQueue in the standard class library.

Refer to the source code below. First, obtain the condition variable through the reentry lock:


/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;
 
public ArrayBlockingQueue(int capacity, boolean fair) {
  if (capacity <= 0)
      throw new IllegalArgumentException();
  this.items = new Object[capacity];
  lock = new ReentrantLock(fair);
  notEmpty = lock.newCondition();
  notFull =  lock.newCondition();
}
复制代码

Two condition variables are created from the same reentrant lock , and then used in specific operations, such as the take method below, to determine and wait for the condition to be met:

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
      while (count == 0)
          notEmpty.await();
      return dequeue();
  } finally {
      lock.unlock();
  }
}
复制代码

When the queue is empty, the correct behavior of the thread trying to take should be to wait for enqueue to occur instead of returning directly. This is the semantics of BlockingQueue. This logic can be elegantly implemented using the conditional notEmpty.

So, how to ensure that joining the queue triggers subsequent take operations? Please look at the enqueue implementation:

private void enqueue(E e) {
  // assert lock.isHeldByCurrentThread();
  // assert lock.getHoldCount() == 1;
  // assert items[putIndex] == null;
  final Object[] items = this.items;
  items[putIndex] = e;
  if (++putIndex == items.length) putIndex = 0;
  count++;
  notEmpty.signal(); // 通知等待的线程,非空条件已经满足
}
复制代码

Through the combination of signal/await, condition judgment and notification waiting threads are completed, and the status flow is completed very smoothly. Note that it is very important to call signal and await in pairs, otherwise assuming there is only await action, the thread will wait until it is interrupted.

From a performance perspective, the early implementation of synchronized was relatively inefficient. Compared with ReentrantLock, the performance of most scenarios is quite different. However, many improvements have been made to it in Java 6. You can refer to the performance comparison. In high competition situations, ReentrantLock still has certain advantages. My detailed analysis in the next lecture will be more helpful in understanding the underlying reasons for the performance differences. In most cases, there is no need to worry about performance, but consider the convenience and maintainability of the code writing structure.  

postscript

That's it for  Java: What's the difference between synchronized and ReentrantLock?  all the content;

It introduces what thread safety is, compares and analyzes synchronized and ReentrantLock, and introduces condition variables and other aspects with case code.

Guess you like

Origin blog.csdn.net/2301_76607156/article/details/130525557