Did you use the right lock? Talking about Java "locks"

In every age, we will never treat people who can learn

Hello everyone, I am yes.

I originally planned to continue writing about the message queue, but recently I was bringing a new colleague, and found that the new colleague has some misunderstandings about locks, so today I will talk about "locking" and the use of concurrent security containers in Java. .

But before that, we still have to come to the first why we need to lock this stuff, this has to start from the source of the concurrent BUG.

The source of concurrent bugs

I wrote an article on this question in 19 years, and I am really shy looking back at that article.

Let's take a look at the source of this. We know that the computer has CPU, memory, and hard disk. The hard disk has the slowest reading speed, followed by the memory reading. The memory reading is too slow compared to the CPU operation. Another CPU cache, L1, L2, and L3.

It is this CPU cache coupled with the current multi-core CPU situation that has generated concurrent BUG .

This is a very simple code. If thread A and thread B execute this method in CPU-A and CPU-B respectively at this time, their operation is to first access a from the main memory to the respective cache of the CPU. When the value of a in their cache is 0.

Then they execute a++ respectively. At this time, the value of a in their eyes is 1, and then the value of a is still 1 when brushing a to the main memory. This is a problem. Obviously, the final result of adding one to two times It's 1, not 2.

This problem is called a visibility problem .

Looking at our a++ statement, our current languages ​​are all high-level languages. This is actually very similar to syntactic sugar. It seems to be very convenient to use. In fact, it is just the surface, and there is an instruction that really needs to be executed.

When a sentence in a high-level language is translated into a CPU instruction, there can be more than one. For example, there are at least three a++ instructions that are converted into a CPU instruction.

  • Take a from the memory to the register;

  • +1 in the register;

  • Write the result to the cache or memory;

So we think that the statement a++ is impossible to interrupt and is atomic, but in fact, the CPU may be able to execute an instruction time slice. At this time, the context switches to another thread, and it also executes a++. When you cut back again, the value of a is actually incorrect.

This problem is called the atomic problem .

In addition, the compiler or interpreter may change the execution order of statements in order to optimize performance. This is called instruction rearrangement. The most classic example is the double check of the singleton mode. In order to improve execution efficiency, the CPU also executes out of order. For example, when the CPU is waiting for the memory data to be loaded, it finds that the subsequent addition instruction does not depend on the calculation result of the previous instruction, so it executes the addition instruction first.

This problem is called the problem of order .

So far, the source of concurrent BUG has been analyzed, namely these three problems. It can be seen that whether it is CPU cache, multi-core CPU, high-level language, or out-of-order rearrangement is actually necessary, so we can only face these problems directly.

To solve these problems is to disable the cache, prohibit the reordering of compiler instructions, and mutual exclusion. Today, our topic is related to mutual exclusion.

Mutual exclusion is to ensure that the modification of shared variables is mutually exclusive, that is, only one thread is executing at the same time. When it comes to mutual exclusion, I believe that what comes to mind is the lock . Yes, our theme today is locks! Locking is to solve the problem of atomicity.

lock

When it comes to locking, the first reaction of Java students is the synchronized keyword, after all, it is supported at the language level. Let's take a look at synchronized first. Some students don't understand synchronized well, so there will be many pitfalls in using it.

synchronized note

Let's first look at a code. This code is our way to raise wages. In the end, millions are sprinkled. A thread compares whether our wages are equal. Let me briefly say that IntStream.rangeClosed(1,1000000).forEachsome people may not be familiar with this. This code is equivalent to 100W for loops.

First understand it for yourself and see if there are any problems? The first reaction seems to be okay. When you look at the salary increase, it is executed in one thread. There is no change in the value of the salary. It seems that there is nothing wrong with it? There is no competition for concurrent resources, and volatile modification is used to ensure visibility.

Let's take a look at the results, I have taken some screenshots.

It can be seen that it is already wrong if the log is typed first, and the value that is typed out is the same ! Is it out of your expectation? Some students may subconsciously think that this raiseSalaryis a modification, so it must be a thread safety issue to raiseSalaryadd a lock!

Please note that only one thread is calling the raiseSalarymethod, so just raiseSalarylocking the method is useless.

This is actually the atomicity problem I mentioned above. Imagine that when the salary increase thread is executed and has yesSalary++not been executed yourSalary++, it is executed just to the point yesSalary != yourSalarythat the salary thread is executed. Is it definitely true? So it will print out the log.

Furthermore, because the visibility is guaranteed by the volatile modification, when logging yourSalary++is executed, the execution may have been completed, and the log output at this time will be yesSalary == yourSalary.

So the easiest solution is to raiseSalary()and compareSalary()are synchronized with the modification, and so raise wages than two threads will not perform at the same time, so be sure to be safe!

It seems that the lock is quite simple, but the use of this synchronized is still a pitfall for novices, that is, you have to pay attention to what the synchronized lock is.

For example, I changed to multi-thread to increase salary. Here again parallel, this is actually the use of ForkJoinPool thread pool operation, the default number of threads is the number of CPU cores.

Due to raiseSalary()the addition of a lock, so the end result is right. This is because the synchronized modification is yesLockDemoan instance, and there is only one instance in our main, so it is equivalent to a lock in multi-thread competition, so the final calculated data is correct.

Then I will modify the code so that each thread has a yesLockDemo instance to increase the salary.

You will find why this lock is useless? This is said to be a good one million annual salary, I will become 10w? ? Fortunately, you still have 70w.

This is because at this time our lock is decorated with a non-static method, an instance-level lock , and we have created an instance for each thread, so these threads are not competing for a lock at all , and the above multithreading The correct code is calculated because each thread uses the same instance, so the contention is a lock. If you want the code at this time to be correct, you only need to change the instance-level lock into a class-level lock .

It is very simple to just turn this method into a static method. Synchronized modified static method is a class-level lock .

The other is to declare a static variable, which is more recommended, because turning a non-static method into a static method actually changes the code structure.

Let's summarize. When using synchronized, you need to pay attention to what the lock is. If you modify static fields and static methods, it is a class-level lock. If you modify non-static fields and non-static methods, it is an instance-level lock .

Lock granularity

I believe everyone knows that Hashtable is not recommended. If you want to use it, use ConcurrentHashMap because Hashtable is thread-safe, but it is too rude. It locks all methods with the same lock! Let's look at the source code.

What do you think is the relationship between contains and the size method? Why don't I let me adjust the size when I call contains? This is that the granularity of the lock is too coarse. We have to evaluate it. Different methods use different locks so that the concurrency can be improved under the condition of thread safety.

But different locks in different methods are not enough, because sometimes some operations in a method are actually thread-safe, and only the piece of code that involves competing resources needs to be locked . Especially when the code that does not require a lock is time-consuming, it will occupy the lock for a long time, and other threads can only wait in line, such as the following code.

Obviously the second piece of code is the normal posture of using the lock, but in the usual business code, it is not as easy to see at a glance like the sleep posted in my code. Sometimes it is necessary to modify the order of code execution, etc. To ensure that the granularity of the lock is fine enough .

Sometimes it is necessary to ensure that the lock is thick enough, but this part of the JVM will detect it and it will help us to optimize it, such as the following code.

It can be seen that the logic that is called in a method has been experienced 加锁-执行A-解锁-加锁-执行B-解锁, and it is obvious that only experience is required 加锁-执行A-执行B-解锁.

Therefore, the JVM will coarsen the lock during just-in-time compilation, and expand the scope of the lock, similar to the following situation.

And JVM will also have the action of lock elimination . Through escape analysis to determine that the instance object is thread-private , it must be thread-safe, so it will ignore the lock action in the object and call it directly.

Read-write lock

The read-write lock is what we submitted above to reduce the granularity of the lock according to the scenario. The lock is split into a read lock and a write lock, which is especially suitable for use in the case of more reads and less writes , such as a cache implemented by yourself.

ReentrantReadWriteLock

The read-write lock allows multiple threads to read shared variables at the same time , but the write operations are mutually exclusive, that is, mutually exclusive write and write, and mutually exclusive read and write. To put it bluntly, when writing, only one thread can write, and other threads can neither read nor write.

Let's look at a small example, which also has a small detail. This code is to simulate the reading of the cache. First, the read lock is applied to the cache to fetch the data. If the cache has no data, the read lock is released, and then the write lock is applied to the database to fetch the data, and then the data is inserted into the cache to return.

There's the small details that determine again data = getFromCache()whether there is value, because the same time there may be multiple threads call getData(), then the cache write lock is empty so they have to compete, and ultimately only one thread will first get a write lock, and then the data Stuffed into the cache.

At this time, the waiting threads will eventually get the write lock one by one. When the write lock is acquired, there is actually a value in the cache, so there is no need to query the database.

Of course, everyone knows the usage paradigm of Lock, and it needs to be used try- finallyto ensure that it will be unlocked. The read-write lock has another important point to note, that is, the lock cannot be upgraded . What does that mean? Let me change the code above.

However, read locks can be used in write locks to degrade the locks . Some people may ask what read locks are added to the write locks.

It is still useful. For example, a thread grabs the write lock, adds the read lock when the write action is about to complete, and then releases the write lock. At this time, it also holds the read lock to ensure that the write lock operation can be completed immediately. The data, and other threads can also read data because the write lock is gone at this time .

In fact, there is no need for more overbearing locks such as write locks at present! So let's downgrade it so everyone can read it.

To sum up, the read-write lock is suitable for the situation of more reads and less writes. It cannot be upgraded, but it can be downgraded . Lock needs to cooperate try- finallyto ensure that it will be unlocked.

By the way, I will mention the implementation of the read-write lock a little bit . Those who are familiar with AQS may know the state inside. The read-write lock divides this int type state into two halves. The high 16 bits and low 16 bits are recorded and read separately. The state of the lock and write lock. The difference between it and ordinary mutex locks lies in maintaining these two states and processing these two locks in the waiting queue .

Therefore, in scenarios that are not suitable for read-write locks, it is better to directly use mutex locks , because read-write locks also need to perform displacement judgments and other operations on the state.

StampedLock

I also mentioned this thing a little bit, the appearance rate proposed in 1.8 does not seem to be as high as ReentrantReadWriteLock. It supports write lock, pessimistic read lock and optimistic read. Write locks and pessimistic read locks are actually the same as the read-write locks in ReentrantReadWriteLock, which adds an optimistic read.

From the above analysis, we know that the read-write lock is actually unable to write when reading, while the optimistic read of StampedLock allows one thread to write . Optimistic reading is actually the same as the database optimistic lock we know. The optimistic lock of the database is judged by a version field, such as the following sql.

StampedLock optimistic reading is similar to it, let's take a look at simple usage.

The comparison between it and ReentrantReadWriteLock is strong here. Others do not work. For example, StampedLock does not support reentry and condition variables. Another point is that you must not call the interrupt operation when using StampedLock, because it will cause the CPU to be 100% . I ran the example provided on the concurrent programming website and reproduced it.

The specific reasons will not be repeated here. A link will be posted at the end of the article. The above is very detailed.

So something that seems to be very powerful comes out, you need to really understand it, and be familiar with it in order to be targeted.

CopyOnWrite

Copy-on-write will be used in many places, such as process fork()operation. It is also very helpful for our business code level, because its read operation does not block writing, and write operation does not block reading. It is suitable for scenarios with more reading and less writing.

For example, the implementation in Java CopyOnWriteArrayList, someone may hear that this stuff will not block writing when it is thread-safe, so good guys use it!

You have to figure out that copy-on-write will copy a copy of data , and any of your modification actions CopyOnWriteArrayListwill be triggered once in Arrays.copyOf, and then modify on the copy. If there are many modifications and the copied data is also large, this will be a disaster!

Concurrent safe container

Finally, let's talk about the use of concurrent security containers. I will take the relatively familiar ConcurrentHashMap as an example. I think new colleagues seem to think that as long as they use concurrent safe containers, they must be thread safe. In fact, it's not all, it depends on how to use it.

Let's take a look at the following code first. Simply put, it uses ConcurrentHashMap to record each person's salary, up to 100.

The final result will exceed the standard, that is, not only 100 people are recorded in the map. So what is the right result? Very simple is to add a lock.

Seeing this, some people said, if you have locked it, what else do I use ConcurrentHashMap? Adding a lock to my HashMap can be done! Yes you are right! Because our current usage scenario is a composite operation, that is, we first judge the size of the map, and then execute the put method, ConcurrentHashMap cannot guarantee that the composite operation is thread-safe !

ConcurrentHashMap is only appropriate to use the thread-safe methods exposed by it, not in the case of compound operations. For example, the following code

Of course, my example is not appropriate. Actually, because the performance of ConcurrentHashMap is higher than that of HashMap + lock, the reason is the segment lock, which requires multiple key operations to be reflected, but the point I want to highlight is that you can't be careless when using it, and you can't just think it purely. It is thread-safe after use.

in conclusion

Today I talked about the source of concurrency bugs, namely, three major issues: visibility issues, atomic issues, and order issues. Then briefly talk about the attention points of the synchronized keyword, that is, modifying static fields or static methods is a class-level lock, while modifying non-static fields and non-static methods is an instance-level class.

Let's talk about the granularity of locks. Defining different locks in different scenarios cannot be done roughly with a single lock, and the granularity of the locks inside the method should be fine. For example, read-write locks, copy-on-write, etc. can be used in scenarios that read more and write less.

In the end, we must use the concurrent safe container correctly. You should not blindly think that the use of the concurrent safe container must be thread safe. Pay attention to the scenario of compound operations.

Of course I just talked about it briefly today. There are actually many points about concurrent programming. It is not easy to write thread-safe code, just like the whole process of Kafka event processing that I analyzed before, the original version All kinds of locks control concurrency and security. Later, bugs cannot be fixed at all. Multi-threaded programming is difficult, debugging is difficult, and bug fixing is also difficult.

Therefore, the Kafka event processing module is finally changed to a single-threaded event queue mode , which abstracts the access related to shared data competition into events, stuffs the events into the blocking queue, and then single-threaded processing .

So we have to think before using locks, is it necessary? Can it be simplified? Otherwise, you will know how painful it is to maintain afterwards.

At last

After that, I continued to write about the message queue, including RocketMQ and Kafka. Many students left messages in the background and wanted to communicate with me in depth. I have some relationship. I added a contact me in the official account menu. Small partners in need can add My WeChat.

The link for StampedLock bug: http://ifeve.com/stampedlock-bug-cpu/


I am yes, from a little bit to a little bit, see you in the next article .

Previous recommendations:

The mysterious story of the father of Redis about CRC64

Interviewer: Do you know the time wheel algorithm? How is it applied in Netty and Kafka?

Message queue interview hot spot

Things you don't know about the underlying storage of Kafka and RocketMQ

Illustration + code | Common current limiting algorithms and thinking about current limiting in a single-machine distributed scenario

Interviewer: Talk about the entire process of Kafka processing requests

Guess you like

Origin blog.csdn.net/yessimida/article/details/108172551