synchronized and Reentrantlock
In multithreaded programming, we use locks when the code needs to be synchronized. Java provides us with two synchronization methods: built-in lock (synchronized) and explicit lock (ReentrantLock). Explicit locks were introduced by JDK1.5. What are the similarities and differences between these two locks? Is it just an option or is there another reason? This article will find out for you.
// Example of synchronized keyword usage public synchronized void add(int t){// Synchronized method this.v += t;}public static synchronized void sub(int t){// Synchronized static method value -= t;}public int decrementAndGet(){ synchronized(obj){// Synchronized code block return --v; }}This is all about using built-in locks, you've learned it.
The built-in lock is very convenient to use, and does not require explicit acquisition and release. Any object can be used as a built-in lock. Using built-in locks can solve most synchronization scenarios. "Any object can be used as a built-in lock" also means that where the synchronized keyword appears, there is an object associated with it. Specifically:
When synchronized acts on the normal method, the lock object is this;
When synchronized acts on the static method, the lock object is the Class object of the current class;
When synchronized acts on the code block, the lock object is this obj in synchronized(obj).
Explicit lock
The built-in lock is so easy to use, why do you need an extra explicit lock? Because some things cannot be done with built-in locks, such as:
We want to add a waiting time to the lock, and give up before the timeout has been obtained, so that we will not wait infinitely;
We want to acquire the lock in an interruptible way, so that the external thread sends us an interrupt signal and can evoke the thread waiting for the lock;
We want to maintain multiple waiting queues for locks, such as a producer queue and a consumer queue, while improving the efficiency of locks.
Explicit locks (ReentrantLocks) are officially born to solve these flexible needs. ReentrantLock literally means reentrant lock, and reentrant means that the thread can request the same lock multiple times at the same time without causing it to be deadlocked by itself. Here is the difference between a built-in lock and an explicit lock:
Timeable: RenentrantLock.tryLock(long timeout, TimeUnit unit) provides a way to end the waiting time. If the thread does not obtain the lock within the specified time, the method will return false and end the thread waiting.
Interruptible: You must have seen InterruptedException. Many multi-threaded methods will throw this exception. This exception is not a burden caused by a defect, but a necessity, or a good thing. Interruptibility provides us with a way to get a thread to end early (rather than having to wait until the thread execution ends), which is very useful for time-consuming tasks. For built-in locks, the thread will wait for the built-in lock if it cannot get it. There is no other way to end the waiting except to acquire the lock. RenentrantLock.lockInterruptibly() gives us a way to end the wait with an interrupt.
Condition queue: After a thread acquires a lock, it may enter a waiting state because it is waiting for a certain condition to occur (the built-in lock passes through the Object.wait() method, and the explicit lock passes through the Condition.await() method). The thread entering the waiting state will hang and automatically release the lock, and these threads will be put into the condition queue. synchronized has only one conditional queue, while ReentrantLock can have multiple conditional queues. What are the benefits of multiple queues? Please read down.
Conditional predicate: After a thread acquires a lock, it sometimes needs to wait for a certain condition to be met before it can do things. For example, the producer needs to wait until the cache is not satisfied before it can put messages into the queue, while the consumer needs to wait until the cache is not empty before it can retrieve messages from the queue. These conditions are called conditional predicates. The thread needs to acquire the lock first, and then determine whether the conditional predicate is satisfied. If it is not satisfied, it will not be executed downwards. The corresponding thread will give up the execution right and automatically release the lock. Different threads using the same lock may have different conditional predicates. If there is only one conditional queue, when a conditional predicate is satisfied, it is impossible to determine which thread in the conditional queue to wake up; but if each conditional predicate has a separate conditional queue, when a conditional is satisfied, we know that the thread on the corresponding queue should be awakened (the built-in lock is awakened through the Object.notify() or Object.notifyAll() method, and the explicit lock is awakened through the Condition.signal() or Condition.signalAll() method). This is the benefit of multiple conditional queues.
When using built-in locks, the object itself is both a lock and a conditional queue; when using explicit locks, the object of RenentrantLock is a lock, and the conditional queue is obtained through the RenentrantLock.newCondition() method. Multiple conditional queues can be obtained by calling this method multiple times.
A typical example of using an explicit lock is as follows:
// Example of using explicit lock ReentrantLock lock = new ReentrantLock();// Acquire lock, this is the usage corresponding to the synchronized keyword. lock.lock();try{ // your code} finally{ lock.unlock();}// You can time, and give up the lock if you get the lock after the specified time is exceeded. try { lock.tryLock(10, TimeUnit.SECONDS); try { // your code } finally { lock.unlock(); }} catch (InterruptedException e1) { // exception handling}// You can interrupt, and the thread thread 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, please refer to [ArrayBlockingQueue](https://github.com/CarpenterLee/JCRecopes/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 a finally block, which is necessary. An explicit lock will not be automatically released like a built-in lock. When using an explicit lock, it must be manually released in the finally block. If the lock is not released due to an exception after acquiring the lock, then the lock will never be released! Put unlock() in finally block to ensure that it can be released normally no matter what happens.
in conclusion
Built-in locks can solve most scenarios that require synchronization. Only when additional flexibility is required, explicit locks need to be considered, such as timing, interruptible, and multiple wait queues.
Although explicit locks are flexible, they require explicit application and release, and release must be placed in the finally block, otherwise the lock may never be released due to exceptions! This is the most obvious disadvantage of explicit locking.
In summary, when synchronizing, please give priority to safer and easier to use implicit locks.