Link: https://zhuanlan.zhihu.com/p/71156910
Source: Zhihu
Copyright belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.
The topic of locking in Java on the Web is quite informative, but it often feels like a long list of jargon that can be confusing and forgettable after reading. This article is intended to be an easy-to-understand integration for Java newcomers, aimed at removing fear from the terminology of various locks, dabbling at the underlying implementation of each lock, but knowing what to look for when needed.
The first thing to dispel is the idea that a lock can only belong to one category. That’s not the case. For example, a lock can be pessimistic, reentrant, fair, interruptible, etc., just as a person can be a man, a doctor, a fitness enthusiast, a gamer, and there’s no contradiction in that. OK, international practice, dry goods.
○ Synchronized and Lock
There are two ways to Lock in Java: using the synchronized keyword and the implementation class of the Lock interface.
The synchronized keyword is, metaphorically speaking, automatic and can meet all daily driving needs. But if you want to play with drift or all sorts of manipulation, you need a manual shift – the various Lock implementation classes.
So if you want to simply add a lock with no specific performance requirements, use the synchronized keyword. Since Java 5 in Java. Util. Concurrent. The locks package with another way to achieve Lock, that is the Lock. That is to say, the synchronized is built-in Java language keywords, and the Lock is an interface, this interface implementation class can realize the function of the Lock code, details not in this paper, are interested can study AbstractQueuedSynchronizer class, I mean, it’s kind of awesome.
ReentrantLock, ReadLock, and WriteLock are the three most important implementation classes of the Lock interface. They correspond to “reentrant locks”, “read locks”, and “write locks”. Their uses will be explained later.
ReadWriteLock is a factory interface, and ReentrantReadWriteLock is an implementation class of ReadWriteLock that contains two static internal classes ReadLock and WriteLock. These two static inner classes, in turn, implement the Lock interface.
Let’s stop digging into the source code, and just from a usage perspective, what distinguishes Lock from synchronized? In the next few sections, I’ll tease out the concepts of various Lock categories and the differences and connections between the synchronized keyword and various Lock implementation classes.
Pessimistic lock and optimistic lock
A macro classification of locks is pessimistic and optimistic. Pessimistic and optimistic locks do not specifically refer to locks (there is no Lock implementation class in Java called Pessimism Lock or OptimisticLock), but rather two different strategies in a concurrency situation.
Pessimistic Lock, when YOU are Pessimistic, each time you fetch the data you think someone else will change it. So it locks up every time we pick up data. Others who want to take data are blocked until the pessimistic lock is released.
An Optimistic Lock is when you’re so Optimistic that every time you go to get your data, you think no one else is going to change it. So it won’t lock! It won’t lock! However, if you want to update the data, you will check whether anyone else has changed the data in the period between reading and updating. If so, it reads again and tries to update again, repeating the above steps until the update succeeds (and of course allowing the thread that failed to update to abandon the operation).
Pessimistic locks block transactions, and optimistic locks rollback retry. Each has its advantages and disadvantages, and one is not necessarily better than the other. Things like optimistic locking are good for low write situations, where collisions are really rare, which saves on the overhead of locking and increases the overall throughput of the system. However, if there are frequent conflicts, the upper layer application will constantly retry, which can degrade performance, so pessimistic locking is appropriate in this case.
CAS is the basis of optimistic locking
When it comes to optimistic locking, one concept must be mentioned: CAS
What is CAS? Compare-and-swap, Compare and replace, there’s also something called compare-and-set, Compare and Set.
1. Compare: read A value A and check if the original value is still A (not changed by other threads) before updating it to B.
2. Setting: If yes, change A to B. No further action is required. [1] If not, do nothing.
The above two steps are atomic and can simply be interpreted as instantaneous, which the CPU sees as a one-step operation.
With CAS, you can implement an optimistic lock:
data = 123; // Share data /* The thread that updates data does the following */ flag =true;
while(flag) { oldValue = data; // Save raw data newValue =doSomething(oldValue); // The following part is the CAS operation, trying to update the value of dataif(data == oldValue) {// Compare data = newValue; // Set flag =false; / / end}else}} /* Obviously, this code is not atomic at all, because real CAS uses CPU instructions. This is just to show the flow of execution, with the same intention. * /Copy the code
This is a simple and intuitive optimistic lock implementation that allows multiple threads to read at the same time (because there is no locking at all), but only one thread can successfully update data, causing the other threads to rollback and retry. CAS uses CPU instructions to ensure atomicity of operations at the hardware level to achieve a lock-like effect.
Because there are no “lock” and “unlock” operations, optimistic locking strategies are also known as lockless programming. In other words, optimistic locking is not really a lock, it’s just an algorithm that repeats CAS!
Spin lock
There is a type of lock called a spin lock. Spin is basically a while(true) infinite loop.
The optimistic lock has a similar infinite loop operation. Is it a spin lock?
Thanks in the comments section
A cat of shrimpCorrect me.
It isn’t. Although the spin operation is the same as the while(true) operation, the terms should be kept separate. The word “spin” refers specifically to the spin of a spin lock.
There is no such thing as a SpinLock class in the JDK. What is a SpinLock? Read the next section to find out.
Synchronized upgrade: partial lock → lightweight lock → heavyweight lock
As mentioned earlier, the synchronized keyword is like a car’s automatic transmission. Here’s how it works. With one foot on the gas, synchronized moves from no-lock to bias, to lightweight, and finally to heavyweight, like an automatic shift. So where is the spin lock? The lightweight lock in this case is a spin lock.
When a synchronized block is first implemented, a lock object becomes a bias lock (CAS modifies the lock flag bit in the object’s head), which literally means a lock “biased in favor of the first thread to acquire it.” The thread does not actively release bias locks after executing synchronized blocks of code. When a block of synchronized code is reached for the second time, the thread will determine whether the thread holding the lock is its own (the thread holding the lock ID is also in the object head), and if so, proceed normally. Since the lock was not released before, there is no need to re-lock it. If only one thread is using the lock all the time, it is clear that biased locking has little additional overhead and high performance.
Once a second thread enters the lock contention, the bias lock is upgraded to a lightweight lock (spin lock). To clarify what lock contention is, there is no lock contention if multiple threads take turns acquiring a lock, but each time the lock is acquired without blocking, there is no lock contention. Lock contention occurs only when a thread attempts to acquire a lock and finds that the lock is already occupied and has to wait for it to be released.
If the lock contention continues in the lightweight lock state, the thread that did not grab the lock will spin, that is, constantly loop to determine whether the lock can be successfully acquired. The operation of obtaining the lock is to modify the lock flag bit in the head of the object through CAS. First compares whether the current lock flag bit is “released”, if so it is set to “locked”, and the comparison and setting occurs atomically. The thread then modifies the current lock holder information to itself.
Long hours of spinning can be very resource-intensive, with one thread holding the lock and the other threads running on empty CPU, unable to perform any useful tasks, a phenomenon called busy-waiting. If multiple threads use a lock, but no lock contention occurs, or if only a slight lock contention occurs, synchronized uses lightweight locks, allowing for short periods of busy behavior, and so on. This is a tradeoff idea, short periods of busy, etc., in exchange for the overhead of threads switching between user and kernel mode.
Obviously, there is a limit to the number of spins (there is a counter that records the number of spins, allowing 10 cycles by default, which can be changed by vm parameters). If lock contention is serious, a thread that has reached the maximum number of spins will upgrade the lightweight lock to the heavyweight lock (again, the CAS changes the lock flag, but does not change the thread ID that holds the lock). When a subsequent thread tries to acquire a lock and finds that the lock is a heavyweight lock, it suspends itself (rather than being busy) and waits to be woken up in the future. Prior to JDK1.6, synchronized directly weighted locks are clearly now well optimized.
A lock can only be upgraded gradually (also known as lock inflation) in the order of partial lock, light lock, and heavy lock.
Thanks in the comments section
Cool handsome handsome beautiful beautifulThe problem:
One feature of biased locking is that the thread holding the lock does not release the lock after it finishes executing a synchronized block of code. Does a lock contention occur when the second thread executes the synchronized block and then upgrades to a lightweight lock?
When thread B tries to acquire the lock after thread A executes the synchronized code block for the first time, it finds that the lock is biased and determines whether thread A is still alive.
If thread A is still alive,Thread A pauses, at which point the bias lock is upgraded to A lightweight lock, after which thread A resumes execution, thread B spins. but
If the result is thread A doesn’t exist, thread B holds the bias lock and the lock does not upgrade.
5. Reentrant lock (Recursive lock)
A reentrant lock literally means “re-entrant lock” and allows the same thread to acquire the same lock multiple times. For example, if a recursive function has a lock operation, does the lock block itself during recursion? If not, the lock is a reentrant lock (also called a recursive lock for this reason).
Any Lock in Java that starts with the name Reentrant is Reentrant, and all existing Lock implementation classes provided by the JDK, including synchronized locks, are Reentrant. If you need a non-reentrant lock, you have to do it yourself. There are a lot of non-reentrant lock implementations out there, so I won’t post the code here. Reentrant locking is fine for 99% of business scenarios. What is the remaining 1%? I don’t know, who can tell me in the comments?
Six, fair lock, unfair lock
If multiple threads apply for a fair lock, then when the lock is released, the first to apply is the first to get it, very fair. Obviously, if the lock is unfair, the thread that claims it later may acquire the lock first, randomly or in some other order of priority.
For the ReentrantLock class, you can specify whether the lock is fair by passing a constructor argument. The default is unfair. Generally, the throughput of an unfair lock is higher than that of a fair lock. If there is no special requirement, an unfair lock is preferred.
Synchronized is also an unfair lock, but there is no way to make it fair.
Seven, can interrupt the lock
Interruptible locks, which literally means “locks that can respond to interrupts.”
The key here is to understand what interrupts are. Java does not provide any way to interrupt a thread directly, only an interrupt mechanism. What is an interrupt mechanism? Thread A sends thread B A “please stop running” request (thread B can also send this request to itself), but thread B does not stop running immediately. Instead, thread B responds to the interrupt in its own way at an appropriate time, or it can ignore the interrupt. In other words, Java interrupts do not terminate the thread directly; instead, it is up to the interrupted thread to decide what to do. It is like parents telling their children to pay attention to their bodies, but whether and how they pay attention to their bodies is entirely up to them. [2]
Returning to the topic of locks, if thread A holds the lock, thread B waits to acquire it. Because thread A has held the lock too long and thread B doesn’t want to wait any longer, we can have thread B interrupt itself or interrupt it in another thread, which is called A breakable lock.
In Java, synchronized is an uninterruptible Lock, and Lock implementation classes are interruptible locks, you can briefly look at the Lock interface.
/* Lock interface */ public interface Lock {void Lock (); // Wait until you get the lock. void lockInterruptibly() throws InterruptedException; InterruptedException is required if an interrupt request is received while waiting. boolean tryLock(); Return immediately whether you can get the lock or not. Get a returntrue, can not get backfalse. boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // As above, you can customize the waiting time. void unlock(); Condition newCondition(); }Copy the code
Read/write lock, shared lock, mutex lock
Read/write locks are actually a pair of locks, a read lock (shared lock) and a write lock (mutex, exclusive lock).
Take a look at the ReadWriteLock interface in Java, which specifies only two methods, one that returns a read lock and one that returns a write lock.
Remember the optimistic locking strategy? All threads can read at any time and only determine if the value has been changed before writing.
Read/write locks actually do the same thing, but the strategy is slightly different. In many cases, threads know if they are reading data to update it. So why not be explicit about this when locking? If I read the value to update it, then the write lock is added directly to the lock. While I hold the write lock, other threads either read or write the value and wait. If I read data only for front-end display, then I explicitly add a read lock when I add a lock, and other threads can acquire the read lock without waiting (read lock counter +1).
Although read-write locks feel similar to optimistic locks, read-write locks are pessimistic locking strategies. This is because the read-write lock does not determine whether the value has been modified before the update, but rather determines whether a read-write lock should be used before the lock is added. Optimistic locking specifically refers to lockless programming. If you’re still confused, go back to sections 1 and 2 to see what “optimistic locking” is.
The only class that the JDK provides to implement the ReadWriteLock interface is ReentrantReadWriteLock. As the name suggests, it provides not only read-write locks, but also reentrant locks. In addition to the two interface methods, ReentrantReadWriteLock provides a number of ways for outsiders to monitor its internal working state, which are not covered here.
Back to the pessimistic lock and the optimistic lock
This article has undergone a revision. I used to think that biased Lock and lightweight Lock are optimistic locks, while heavyweight Lock and Lock implementation class are pessimistic locks. Many materials on the Internet have vague expressions about these concepts, with different opinions.
Here’s my conclusion:
Almost all of the locks we use in Java are pessimistic locks. Synchronized from bias lock, lightweight lock to heavyweight lock, all pessimistic lock. The Lock implementation classes provided by the JDK are all pessimistic locks. In fact, as long as there is a “lock object”, so it must be pessimistic lock. Because optimistic locking is not a lock, but an algorithm that tries CAS in a loop.
Is there an optimistic lock in the JDK?
There is. Java. Util. Concurrent. Atomic package inside of atomic classes are using optimistic locking.
Why do some data on the net think biased lock, lightweight lock is optimistic lock? Because they use CAS underneath, right? Or is “optimistic/pessimistic” confused with “light/weight”? In fact, when the thread preempts these locks, it does loop +CAS operation, which feels like optimistic lock. But the point is that when we say whether a lock is a pessimistic lock or an optimistic lock, we should always look at the application layer and see how they lock the application data, not at the bottom of the lock grab process. If a thread tries to acquire a lock and finds that it is already occupied, does it continue reading data and decide whether to retry when it wants to update later? For partial locks, lightweight locks, obviously the answer is no. Whether pending, busy, etc., reading of application data is blocked. From this perspective, they are indeed pessimistic locks.
To say the least, there’s no need to get too bogged down in these terms. The most important thing is to understand how they work. I wanted to write as simple as possible, but I found that I had only scratched the surface with nearly ten thousand words. I know my level is limited, I can’t guarantee to be completely correct. I can only say that there is a long way to go, I see no way to correct you.
reference
- The problem is that A value changes from A to B, and from B back to A. In this case, CAS might think that the value has not changed, when in fact it has. An AtomicStampedReference packet is sent to determine the version number.
- If you need in-depth communication, you are welcome to scan the following TWO-DIMENSIONAL code and technical exchange group