Explicit locking mechanism of Java Concurrent Programming
We have previously introduced the synchronized keyword to realize the atomic operation of the program. Its internal is also a locking and unlocking mechanism. It is a declarative programming method. We only need to declare the method or code block. Java helps us lock and unlock before and at the end of calling the method. The explicit lock we will introduce in this article is a manual implementation method. Programmers control the specific implementation of the lock. Although it is more and more likely to use synchronized to directly implement atomic operations, understanding the specific implementation mechanism of the lock interface will help us to use synchronized. This paper mainly involves the following contents:
1、 Basic components of interface lock
Lock is located in Java util. concurrent. Under the locks package, the source code is as follows:
Among them,
void lock();: Calling this method will obtain a lock entry lockinterruptable (): this method is also used to obtain a lock, but it responds to an interrupt. Once an interrupt is encountered in the process of obtaining, it will throw an interruptedexception. boolean tryLock();: This method attempts to obtain a lock. If the acquisition fails, it will return false and will not block the current thread. Boolean trylock (long time, timeunit unit): it attempts to obtain a lock. If the acquisition fails, it will block the waiting time. During the period, if the lock can be obtained, it will return true. Otherwise, it will return false and respond to the interrupt request. void unlock();: Release a lock condition newcondition();: Conditional variables, to be learned in the next article
2、 Basic use of reentrant lock
Reentrantlock is the most important implementation class of interface lock. It not only implements the basic lock adding and lock releasing methods in lock, but also extends its own methods. It has two construction methods:
The parameter fair is used to ensure the fair policy of the lock mechanism. The fair policy is that the longer the waiting time, the thread will get the lock first. Ensuring fairness will inevitably reduce performance, so reentrantlock does not guarantee fairness by default. We use reentrantlock to realize the atomic operation of the program:
When we start 100 threads in the main program and randomly wake up to add one hour to the count, no matter how many times it is run, the result is 100, that is, our reentrantlock can ensure atomic operation for us.
Reentrant lock also has a feature of reentrant, that is, on the premise of obtaining a lock, you can enter other methods locked by the lock at will, and you can enter a lock repeatedly. In addition, reentrantlock also has some other methods for lock information:
3、 The implementation principle of reentrantlock is deeply
Reentrantlock relies on CAS and locksupport. Locksupport is a bit like a tool class. It mainly provides two types of methods, park and unpark.
Calling the park method will cause the current thread to lose CPU usage and change from runnable state to waiting state. The unpark method in turn makes a thread in the waiting state change to runnable and wait for the operating system to schedule. Parknanos and parkuntil are time-dependent variants of two parks. Parknanos specifies the time the thread will wait, and parkuntil specifies the time the thread will wait. This time is an absolute time, relative to the milliseconds of the era.
There are many concurrency tools in the Java concurrency package, such as reentrantreadwritelock, semaphore, countdownlatch, reentrantlock, etc. These tools have many common features, so Java abstracts a class abstractqueuedsynchronizer (AQS) for us to represent the commonness of these tools. Reentrantlock is an implementation class, which has three internal classes:
Sync inherits AQS and implements most of its codes simply. Fairsync and nonfairsync are defined for fair policies. If fair policies are specified when reentrantlock is constructed, all its internal methods rely on fairsync, otherwise they all rely on nonfairsync. Next, look at the constructor of reentrantlock:
The two construction methods will eventually initialize sync, and the of sync will play a considerable role in the subsequent methods. Let's first look at the specific implementation of the lock method:
The lock method of reentrantlock calls the lock method of sync, and the lock method in sync is an abstract method, that is, the specific implementation of this method is in the subclass. Let's look at the implementation in nonfairsync:
There is an integer type state variable in AQS, which is used to identify the number of times the current lock is held. A value of 0 indicates that the current lock is not held by any thread. Compareandsetstate is a method in AQS, which calls unsafe Compareandswapint method updates the state in CAS mode. If the value of state is 0, it means that the lock is not held by any thread. Then the current thread will hold the lock and set the value of state to 1.
This completes the acquisition action. Once the subsequent thread attempts to access the critical area code, acquire (1) will be called before the previous thread does not release the lock.
Tryacquire still calls the implementation in AQS,
The first if determines whether the lock to be held is held (although it has been determined before, it may be released by the previous thread during our call to the nonfairtryacquire method). If it is not held by any thread, it will directly hold the lock.
The second if determines that if the current lock holder is the current thread, it means that this is a reentry operation of the same thread, so increase the number of locks and set the value of state.
After the end of the whole method, if the current thread obtains the lock, it will return true, otherwise it will return false. If the tryacquire method returns true, the entire acquire method will also end. Otherwise, it means that the current thread has not passed the lock and needs to be blocked. Then the acquirequeueueueued (addwaiter (node. Exclusive), Arg) method will be called.
The addwaiter method wraps the current thread into a node node, adds it to a waiting queue maintained within AQS, and returns the node node. Finally, the acquireQueued method is called:
This method will first obtain the previous node of the node. If it is a head node, it indicates that the current node is the first waiting node in the whole waiting queue. So let it try to get the lock. If it can get the lock, it will be cleared from the waiting queue and returned.
If it is found that there are waiting nodes in front of the current node or the attempt to obtain the lock fails, the shouldparkafterfailedacquire method will be called to determine whether the thread corresponding to the node lock needs to be unpark blocked, and finally call locksupport Park (this) blocks the current thread.
On the premise that the first thread holds the lock, the second thread is blocked successfully. This is probably the call chain flow of the whole lock method.
Next, let's look at the specific implementation of unlock,
This is the concrete implementation of the unlock of AQS in reentrantlock. It calls the release method of sync, which is the method in its parent class AQS:
Tryrelease is rewritten by sync. The specific code is as follows:
First, judge if the current thread is not the current holder of the lock, Throw exception (the lock cannot be released without holding the lock). If C is equal to 0, it means that the current lock is held only once, that is, the current thread does not re-enter the lock many times, so set the holder of the lock to null, which means that it is not held by any thread. If C is not equal to 0, it means that the lock is re-entered many times by the current thread, and then set the value of state minus state. Finally If true is returned, the lock is released; otherwise, the current thread still holds the lock.
Return to the release method. If tryrelease (ARG) returns true, the method will judge whether a node in the current waiting queue is waiting for the lock. If so, call the unparksuccess (H) method to wake up the first waiting node thread on the waiting queue and return true.
Here is a detail. In fact, all threads that fail to obtain locks are blocked in the method:
The thread that failed to get the lock was blocked by method parkAndCheckInterrupt, so when we call unpark in unlock to wake up a thread node on the waiting queue, the thread will re enter the dead loop attempt from here to get the lock. If the lock can be obtained, it will remove itself from the waiting queue and return, otherwise it will be blocked again and wait for wake-up.
The whole execution process of the unlock method has also been roughly introduced. Finally, let's take a look at some comparisons between reentrant lock and synchronized.
4、 Reentrantlock vs. synchronized
Synchronized is more inclined to a declarative programming method. We use synchronized modification before the method. Java will automatically implement its internal details for us. It is responsible for when to lock and when to release the lock. For our reentrant lock, we need to manually add and release the lock, which has higher requirements for logic and is relatively more difficult. With the update and optimization of the JVM version, the performance difference between reentrantlock and synchronized is gradually narrowing. Therefore, it is generally recommended to use synchronized to avoid complex and difficult reentrantlock.
The basic information of explicit lock is described above. If there are errors, please point them out!
The above is the whole content of this article. I hope it will be helpful to your study, and I hope you can support programming tips.