Redis is an inescapable skill point in both interview and actual work. In my mind, MySQL and Redis are the two yardsticks to measure whether a programmer can “make something small”. If he can skillfully use MySQL and Redis to reduce the size of the small and make full use of existing resources to fulfill the current demand, it shows that he has grown up.
This article will talk from JVM synchronized, ReentrantLock to Redis distributed lock, does not involve the specific code implementation, but in a simple way to introduce the idea and general principle, very friendly to small white ~
The JVM lock
By JVM locks, we mean locks implemented by things like synchronized keyword or ReentrantLock. It’s collectively called JVM locks because our projects actually run on JVMS. In theory, each project starts up with a piece of JVM memory, and the subsequent execution of the data is in this piece of land.
What is a lock and how?
Now that we know where the name “JVM lock” came from, let’s talk about what a lock is and how to do it.
Sometimes it’s hard to say what something is, but it’s easy to say what it does, and the same goes for JVM locks. JVM locks were invented to address thread safety issues. The so-called thread-safety problem can be simply understood as data inconsistency (inconsistent with expectations).
When can thread safety problems occur?
Thread-safety problems can occur only when the following three conditions are met:
- Multithreaded environment
- There is shared data
- Multiple statement operations sharing data/single statement itself non-atomic operations (e.g. I++ is a single statement but not an atomic operation)
For example, if threads A and B perform +1 operations on int count at the same time (the initial value is assumed to be 1), there is A probability that the result of both operations will be 2 instead of 3.
So why does locking solve this problem?
Regardless of atomicity, memory barriers and other obscure terms, the core of what makes locking thread-safe is mutual exclusion. Mutual exclusion is literally mutual exclusion. Who do we mean by “mutual”? Between threads!
How do you implement mutual exclusion between multiple threads?
Just bring in the middleman.
Notice, this is a very simple and great idea. In the programming world, there are numerous examples, including but not limited to Spring, MQ, and the introduction of “mediations” that ultimately solve problems. Among the code farmers, there is even a saying that there is no problem that can not be solved by introducing the middle layer.
A JVM lock is essentially a middleman between threads and each other, and multiple threads must ask the middleman’s permission before manipulating the locked data:
The lock acts as the gatekeeper, the only access point through which all threads are interrogated. In the JDK, the most common mechanisms for implementing locks are two factions:
- The synchronized keyword
- AQS
Personally, synchronized keyword is more difficult to understand than AQS, but AQS source code is more abstract. Here we briefly introduce the Java object memory structure and the implementation principle of synchronized keyword.
Java object memory structure
To understand the synchronized keyword, you need to know the memory structure of Java objects. Again, the memory structure of a Java object.
Its existence poses a question: if we had the chance to dissect a Java object, what would we see?
There are two objects on the top right. Just look at one of them. We can observe that the Java object memory structure is roughly divided into several blocks:
- Mark Word (Lock related)
- Metadata pointer (class pointer to the class to which the current instance belongs)
- Instance data (this is the one we usually see)
- Align (padding, related to memory alignment)
If you haven’t looked at the memory structure of Java objects before, you might be surprised: Gosh, I thought Java objects were just properties and methods!
Yes, we are most familiar with instance data and think it is the only one. It is the limitation of this concept that makes it difficult for some beginners to understand synchronized. For example, beginners often wonder:
- Why can any object be a lock?
- Object What is the difference between an Object lock and a class lock?
- What locks are used for the common method of synchronized modification?
- What locks do synchronized modified static methods use?
All of these can be found in the Mark Word in the Memory structure of Java objects:
A lot of you are probably seeing this for the first time, and you’re a little confused, but that’s okay, I’m big, too. It’s all the same.
The Mark Word contains a lot of information, but we can simply think of it as a tag that records lock information. The figure above shows the memory of Java objects in a 32-bit virtual machine, and if you count them carefully, you’ll see that all the bits add up to exactly 32 bits. The architecture of a 64-bit VM is similar and will not be described.
Mark Word divides 2 bits from the limited 32 bits to be used as the lock flag bit. Colloquially speaking, it is to Mark the current lock state.
Since every Java object has a Mark Word that marks the lock state (treating itself as a lock), any object in Java can be used as a synchronized lock:
synchronized(person){
}
synchronized(student){
}
Copy the code
The “this” lock is the current object, and the “Class” lock is the Class object of the current object, which is essentially a Java object. The plain methods of synchronized modifications use the current object as the lock underneath, while the static methods of synchronized modifications use the Class object as the lock underneath.
But to ensure that multiple threads are mutually exclusive, the most basic condition is that they use the same lock:
It does not make sense to add two different locks to the same piece of data. In practice, you should avoid the following:
synchronized(Person.class){
/ / operation count
}
synchronized(person){
/ / operation count
}
Copy the code
or
public synchronized void method1(a){
/ / operation count
}
public static synchronized void method1(a){
/ / operation count
}
Copy the code
Synchronized and lock upgrades
With an overview of Java object memory structures behind us, let’s tackle a new puzzle:
Why do we need to mark the lock status? Does that mean synchronized locks have multiple states?
In earlier JDK versions, the implementation of the synchronized keyword was directly based on heavyweight locks. As long as synchronized is used in our code, the JVM will apply to the operating system for locking resources (whether or not the current environment is truly multithreaded), and applying to the operating system for locking resources is expensive, involving switching between user mode and kernel mode, etc., and generally time-consuming and low performance.
To address the issue of poor JVM lock performance, the JDK introduced ReentrantLock, which is based on CAS+AQS, similar to spin locks. Spin means that in the event of lock contention, the thread that has not won the lock will spin outside the door and wait for the lock to be released.
The advantage of spin locking is that spin waiting can be implemented at the JVM level without having to make a dramatic switch to kernel mode to apply a heavyweight lock on the operating system. However, there is no panacea for all benefits in the world. Although CAS spin avoids complex operations such as state switching, it will consume part of CPU resources. Especially when the expected lock time is long and the concurrency is high, hundreds or thousands of threads will spin at the same time, greatly increasing the burden of CPU.
Synchronized is the parent of the JDK, so in JDK1.6 or earlier, the official optimization of synchronized was introduced with the concept of “lock upgrade”, which divides the lock into multiple states.
- unlocked
- Biased locking
- Lightweight locks (spinlocks)
- Heavyweight lock
Locking is the newly created state of a Java object. When this object is accessed for the first time by a thread, the thread “stickers” its thread ID onto its head (partially modified digits in the Mark Word) to indicate “you are mine” :
There is no lock contention, so there is no blocking or waiting.
Why do we design a biased lock?
Recall, are there really so many concurrent scenarios in the project? Not really. For most projects, a variable will be executed by a single thread most of the time, so it is not necessary to apply for a heavyweight lock directly to the operating system, since thread-safety issues will never occur.
Once lock competition occurs, synchronized will upgrade to lightweight lock under certain conditions, which can be understood as a spin lock. The JDK also has a related control mechanism, you can learn by yourself how much spin and when to give up spin.
Synchronized also suffers from the problem of ReentrantLock because it is also spin: what if the lock is long and the spin thread is numerous?
In essence, the operating system will maintain a queue, exchange space for time, avoid the CPU performance cost of multiple threads spinning waiting at the same time, and wake up the waiting thread to participate in a new round of lock competition when the last thread ends.
Extensive reading (not necessary) :
Thread Safety (Middle)– Fully understand synchronized(from bias to heavyweight)
Synchronized low-level implementation — biased locking
Synchronized case
Let’s take a look at a few examples to deepen our understanding of synchronized.
- Are synchronized method m1 and method m2 in the same class mutually exclusive?
The t1 thread reads this lock when executing m1, but the T2 thread does not read this lock.
- Can synchronized method m2 be called from synchronized method m1 in the same class?
Synchronized is a ReentrantLock, which can be roughly understood as a thread that already holds the lock and can acquire it again and perform a +1 operation on a certain amount of state (ReentrantLock also supports reentrant).
- Can subclass synchronized method m call parent synchronized method m?
Before the subclass object is initialized, the parent constructor is called, which is structurally equivalent to wrapping a parent object with this lock object.
- Are statically synchronized and non-statically synchronized methods mutually exclusive?
Each plays its own, not the same lock, not mutually exclusive.
Redis distributed lock concept
When it comes to Redis distributed locks, one question or another always arises:
- What is distributed
- What is distributed locking
- Why do we need distributed locks
- How does Redis implement distributed locking
The first three questions can be answered together, but how Redis implements distributed locking will be covered in the next article.
What is distributed? It’s a very complicated concept, and I’m not sure it’s accurate, so let’s draw a picture and let’s see how it works:
A significant feature of distribution is that Service A and Service B are most likely not deployed on the same server, so they do not share the same JVM memory. As mentioned above, to achieve thread exclusion, you must ensure that all accessing threads use the same lock (JVM locks cannot guarantee mutual exclusion at this point).
For distributed projects, there are as many JVM memory slices as there are servers, and even if there is a “unique” lock on each slice of memory, the locks in the project as a whole are not unique.
At this point, how do you ensure that threads on each JVM share a lock?
The answer is: extract the lock and let threads meet on the same piece of memory.
However, the lock cannot exist in a vacuum, the essence is still in memory, at this time can use Redis cache as the lock host environment, that is why Redis can construct distributed locks.
What is the lock length of Redis
The synchronized keyword and ReentrantLock, both of which are actual implemented locks, have flag bits and so on. But Redis is a memory… How can it be a lock?
The reason why Redis can be used for distributed locking is not just because it is a piece of memory. Otherwise, the JVM also owns memory, so why can’t it implement distributed locking itself?
My personal understanding is that in order to customize a distributed lock, at least a few conditions must be met:
- Multi-process visibility (a piece of memory independent of a multi-node system)
- Mutually exclusive (either through a single thread or through an election mechanism)
- reentrant
Redis can do all three. Under the above three conditions, how you design a lock depends entirely on how you define it. Just like in real life, a lock is a small metal object that has a keyhole and needs to be inserted into it. However, there are more than one type of lock. With the development of technology, fingerprint locks and iris locks emerge one after another, but in the final analysis, they are called “locks” because they guarantee mutual exclusion (I can, you can’t).
If we can design a logic that causes a “mutually exclusive event” in a particular scenario, then it can be called a “lock.” For example, a famous online store only receives one customer a day. There was no clerk at the door, so there was a machine with a ticket in it. If you arrive late, the ticket is gone and you can’t get into the store. In this scenario, customers without tickets can’t get in and are locked out. At this point, the ticket pickup machine causes a “mutually exclusive event,” which is then called a “lock.”
Redis provides the setnx directive, which returns true if a key does not currently exist, or false if it is not set again. Isn’t that the programming equivalent of a number taker? Of course, the actual use of the command can be more than this one, how to implement, see the next article ~
This article from the JVM lock to Redis distributed lock, but also introduced the Java object memory structure and synchronized underlying principle, I believe that we have their own perceptual understanding of “lock”. In the next article, we will introduce the usage scenario of Redis distributed lock through the case of distributed scheduled task.
I’m bravo1988. See you next time.
Scot ro si · Feeling the mezzo
Previous articles:
What if the company forbids JOIN queries?
Get to the basics of Java threads
Simple Java annotations
Tomcat: Lonely kitten