Detailed explanation of reentrantlock and condition in Java multithreading
1、 Reentrantlock class
1.1 what is reentrantlock
java. util. concurrent. The lock framework in lock is an abstraction of locking, which allows the implementation of locking as a Java class rather than as a feature of the language. This leaves room for a variety of lock implementations, which may have different scheduling algorithms, performance characteristics or locking semantics. The reentrantlock class implements lock, which has the same concurrency and memory semantics as synchronized, but adds some features like lock voting, timed lock waiting and interruptible lock waiting. In addition, it provides better performance in the case of fierce contention. (in other words, when many threads want to access shared resources, the JVM can spend less time scheduling threads and more time executing threads.)
What does a reentrant lock mean? In short, it has a lock related acquisition counter. If a thread that owns the lock gets the lock again, the acquisition counter will increase by 1, and then the lock needs to be released twice to get the real release. This mimics the semantics of synchronized; If a thread enters the synchronized block protected by the monitor it already owns, the thread is allowed to continue. When the thread exits the second (or subsequent) synchronized block, the lock is not released. The lock is released only when the thread exits the first synchronized block protected by the monitor it enters.
1.2 comparison between reentrantlock and synchronized
Same: reentrantlock provides similar functionality and memory semantics to synchronized.
Different:
(1) Reentrantlock is more comprehensive in terms of functionality, such as time lock waiting, interruptible lock waiting, lock voting, etc., so it is more extensible. Reentrantlock is more suitable for places with multiple condition variables and highly competitive locks. Reentrantlock also provides conditions, which makes it more flexible to wait for and wake up threads. A reentrantlock can have multiple condition instances , so it is more scalable.
(2) Reentrantlock performs better than synchronized.
(3) Reentrantlock provides a pollable lock request. It can try to obtain the lock. If it succeeds, it will continue to process. If it fails, it can be processed at the next run, so it is not easy to generate a deadlock. Once the lock request is entered, it will either succeed or block all the time, so it is easier to generate a deadlock.
1.3 extended functions of reentrantlock
1.3. 1 implement pollable lock requests
In internal locks, deadlocks are fatal -- the only way to recover is to restart the program, and the only way to prevent it is not to make mistakes when building the program. The pollable lock acquisition mode has a more perfect error recovery mechanism, which can avoid deadlock.
If you can't get all the locks you need, use the pollable acquisition method to enable you to regain control. It will release the locks you have obtained and try again. The pollable lock acquisition mode is implemented by the trylock () method. This method acquires the lock only when the lock is idle when called. If the lock is available, the lock is acquired and the value true is immediately returned. If the lock is not available, this method immediately returns the value false. Typical usage statements of this method are as follows:
1.3. 2 realize timed lock request
When an internal lock is used, once the request is started, the lock cannot be stopped, so the internal lock poses a risk to the implementation of activities with time limit. In order to solve this problem, timing lock can be used. When there is a time limit
The blocking method is called automatically, and the timing lock can set the corresponding timeout within the time budget. If the activity fails to get results within the expected time, the timing lock can make the program return in advance. The timed lock acquisition mode is implemented by the trylock (long, timeunit) method.
1.3. 3. Implement interruptible lock acquisition request
Interruptible lock acquisition operations are allowed in cancellable activities. The lockinterrupt () method enables you to respond to interrupts when you get a lock.
1.4 disadvantages of reentrantlock and points needing attention
(1) The lock must be released in the finally block. Otherwise, if the protected code throws an exception, the lock may never be released! This difference may seem insignificant, but in fact, it is extremely important. Forgetting to release the lock in the finally block may leave a time bomb in the program. When the bomb explodes one day, you will have to spend a lot of effort We found the source. With synchronization, the JVM ensures that the lock is automatically released
(2) When the JVM uses synchronized to manage lock requests and releases, the JVM can include lock information when generating thread dumps. These are very valuable for debugging because they can identify the source of deadlocks or other abnormal behaviors. The lock class is just an ordinary class, and the JVM does not know which thread has the lock object.
2、 Condition variable condition
Condition variables are largely used to solve object Wait / notify / notifyAll is difficult to use.
Let's explain the usage of condition through a practical example:
We want to print 9 numbers from 1 to 9. Thread a prints 1, 2 and 3 first, then thread B prints 4, 5 and 6, and then thread a prints 7, 8 and 9 There are many solutions to this problem. Now we use condition to do this problem
There are complete comments in the above code. Please refer to the comments to understand the usage of condition.
The basic idea is that thread a should first write 1, 2 and 3. At this time, thread B should wait for the reachthredcondition signal. When thread a finishes writing 3, it will tell thread B through the signal "I wrote 3, it's your turn". At this time, thread a should wait for the reachsixcondition signal. At the same time, thread B is notified to start writing 4, 5 and 6, After writing 4, 5 and 6, thread B notifies thread a that the reachsixcondition condition is true. At this time, thread a starts writing the remaining 7, 8 and 9. A condition (also known as a condition queue or condition variable) provides a meaning for a thread to hang (i.e., let it "wait") until another thread that a state condition may now be true notifies it )。 Because access to this shared state information occurs in different threads, it must be protected, so some form of lock should be associated with the condition. The main attribute of waiting to provide a condition is to release the relevant lock atomically and suspend the current thread, just like object Wait did that.
The above API description indicates that condition variables need to be bound to locks, and multiple conditions need to be bound to the same lock. As mentioned in lock, the method to obtain a condition variable is lock newCondition()。
The above is the method defined by the condition interface, and await * corresponds to object Wait, signal corresponds to object Notify, signalall corresponds to object notifyAll。 In particular, the name of the condition interface is changed to avoid confusion with the semantics and use of wait / notify / notifyAll in object, because condition also has the wait / notify / notifyAll method.
Each lock can have a condition object with arbitrary data. Condition is bound to lock, so it has the fairness feature of lock: if it is a fair lock, the thread is from condition. In the order of FIFO If an unfair lock is released in await, the subsequent lock competition does not guarantee the FIFO order.
An example of a producer consumer model using condition is as follows.
In this example, the consumption take () requires that the queue is not empty. If it is empty, it will hang (await ()) until the signal of notempty is received; The production put() requires that the queue is not full. If it is full, it will be suspended (await()) until the notfull signal is received.
Some people may have a problem. If a thread is suspended after the lock () object has not been unlocked, the other thread will not get the lock (the lock () operation will be suspended), and the previous thread cannot be notified. Is this not a "deadlock"?
2.1await * operation
As mentioned in the previous section, reentrantlock is an exclusive lock for many times. If one thread does not release the lock after it gets it, the other thread must not get the lock, so in lock Lock() and lock There may be an operation to release the lock between unlock () (there must also be an operation to obtain the lock). Let's look back at the code, whether take () or put (), before entering lock After lock(), the only operation that can release the lock is await(). In other words, the await () operation actually releases the lock, then suspends the thread, wakes up once the conditions are met, and obtains the lock again!
Above is a snippet of await (). As mentioned in the previous section, AQS needs a CHL FIFO queue when acquiring locks, so for a condition For await (), if the lock is released and you want to acquire the lock again, you need to enter the queue and wait for being notified to acquire the lock. The complete await() operation is performed by installing the following steps:
Adds the current thread to the condition lock queue. In particular, this is different from the AQS queue. Here, the FIFO queue of condition is entered. This structure will be discussed in detail later. Proceed to step 2.
Release the lock. Here you can see that the lock is released, otherwise other threads will not be able to get the lock and issue a life and death lock. Proceed to step 3.
Spin (while) is suspended until it is awakened or timed out, cacelled, etc. Carry out 4.
Acquire lock (acquirequeueueueued). And release myself from the FIFO queue of condition, indicating that I no longer need the lock (I have got the lock).
Here we will go back to the data structure of condition. We know that a condition can be await * () in multiple places, so we need a FIFO structure to connect these conditions in series, and then wake up one or more (usually all) as needed. Therefore, we need a FIFO queue inside the condition.
The above two nodes describe a FIFO queue. In combination with the node data structure mentioned above, we find that node.nextwaiter is useful! Nextwaiter is to connect a series of conditions. Await * to form a FIFO queue.
2.2signal / signalall operation
Await * () is clear. Now it's much easier to look at signal / signalall. According to the requirements of signal / signalall, condition The first node (or all nodes) in the FIFO queue in await * () wakes up. Although all nodes may be waked up, you should know that only one thread can get the lock, and other threads that do not get the lock still need to spin and wait. Step 4 (acquirequeueueueued) mentioned above.
It is easy to see from the above code that signal is to wake up the first non canceled node thread in the condition queue, and signalall is to wake up all non canceled node threads. Of course, when encountering the cancelled thread, you need to remove it from the FIFO queue.
The above is the process of waking up an await * () thread. According to the previous section, if you want to unpark the thread and get the lock, you need the thread node to enter the AQS queue. So you can see in locksupport Unpark called enq (node) before adding the current node to the AQS queue.
summary
The above is all about the detailed explanation of reentrantlock and condition in Java multithreading. I hope it will be helpful to you. If you have any questions, you can leave a message at any time. Xiaobian will reply to you in time.