The fact that one or more operations cannot be interrupted while the CPU is executing is called atomicity. Understanding this feature will help you understand the causes of concurrent programming bugs, such as the weird Bug where long variables can be read and written to 32-bit machines after having been successfully written to memory.
So how do you solve the atomic problem?
You already know that the source of the atomicity problem is thread switching. Wouldn’t it be possible to disable thread switching to solve the problem? The operating system relies on CPU interrupts to do thread switching, so disabling CPU interrupts can prevent thread switching.
This worked in the early days of single-core cpus, and there were many applications, but it was not suitable for multi-core scenarios. To illustrate this problem, we use a 32-bit CPU as an example of a write to a long variable. The long variable is 64-bit, and a write to a 32-bit CPU is split into two writes (write high 32 bits and write low 32 bits, as shown in the figure below).
On single-core CPU scenarios, the same time there is only one thread execution, prohibit the CPU interrupt, means that the operating system, not to schedule threads is also banned thread switches and get right to the use of the CPU thread execution can constantly, so the two write operation must be: either are implemented, or are not implemented, atomicity.
However, in the multi-core scenario, two threads may be executing at the same time. One thread is executing on CPU-1 and the other thread is executing on CPU-2. In this case, CPU interruption is prohibited to ensure the continuous execution of the THREADS on the CPU, but not only one thread at a time. If both threads write long variables 32 bits too high at the same time, then you might get the weird Bug we mentioned at the beginning.
The condition “only one thread is executing at a time” is so important that we call it mutually exclusive. If we can ensure that changes to shared variables are mutually exclusive, then atomicity is guaranteed for both single-core and multi-core cpus.
Simple lock model
When it comes to mutexes, you’re smart enough to think of a killer solution: locks. At the same time, the following models appear in your brain:
We call a section of code that requires mutually exclusive execution a critical section. The thread tries to lock() before entering the critical region. If it succeeds, it enters the critical region. Otherwise, wait until the thread holding the lock unlocks; Unlock () is performed by the thread holding the lock after it has finished executing the code for the critical section.
The process is much like a rush hour pit grab in an office, where everyone goes in to lock the door (lock it) and out to open the door (unlock it). That’s how I understood it for a long time. That’s fine in itself, but it’s easy to overlook two very, very important points: What are we locking? What are we protecting?
Improved lock model
We know that in the real world, locks correspond to the resources that locks protect. For example, you use your lock to protect your stuff, and I use my lock to protect my stuff. In the world of concurrent programming, locks and resources should also have this relationship, but this relationship is not reflected in our model above, so we need to refine our model.
Firstly, we need to mark out the resource to be protected in the critical area. In the figure, an element is added to the critical area: protected resource R; Second, to protect resource R, we need to create a lock for it. Finally, for this lock LR, we also need to add lock operation and unlock operation when entering and leaving the critical region. In addition, I specially use a line to make an association between the lock LR and the protected resource, which is very important. A lot of concurrency bugs occur because we ignore them, and then something like locking our door to protect their property is very difficult to diagnose because subconsciously we think we’ve locked it correctly.
Java language lock technology: Synchronized
Lock is a general technical scheme, and the synchronized keyword provided by Java language is an implementation of lock. Examples of the synchronized keyword, which can be used to modify methods and blocks of code, generally look like this:
class X {
// Decorates non-static methods
synchronized void foo(a) {
/ / critical region
}
// Decorate static methods
synchronized static void bar(a) {
/ / critical region
}
// Decorates the code block
Object obj = newThe Object ();void baz(a) {
synchronized(obj) {
/ / critical region}}}Copy the code
Now, if you look at this, you might be a little bit surprised, but this doesn’t match the model we talked about above, lock lock unlock where is it? The Java compiler automatically adds a lock() and unlock() before and after a synchronized method or block of code. The advantage of this is that lock() and unlock() must come in pairs, because forgetting to unlock() is a fatal Bug (meaning other threads have to wait).
Where is the lock() and unlock() in synchronized? In the above code we can see that only the modifier block locks an obj object. This is also an implicit Java rule:
When you modify a static method, you lock the Class object of the current Class, in this case Class X;
When decorating a non-static method, the current instance object this is locked.
For the example above, synchronized modifies static methods equivalent to:
class X {
// Decorate static methods
synchronized(X.class) static void bar(a) {
/ / critical region}}Copy the code
Pertaining to a nonstatic method equivalent to:
class X {
// Decorates non-static methods
synchronized(this) void foo(a) {
/ / critical region}}Copy the code
Synchronized solve count+=1 problem
If you remember the concurrency problem with count+=1 mentioned in the previous article, you can now try synchronized. SafeCalc has two methods: a get() method that gets the value of a value; The other is the addOne() method, which increments value by 1, and the addOne() method, which we modify with synchronized. So are there any concurrency problems with the two approaches we’re using?
class SafeCalc {
long value = 0L;
long get(a) {
return value;
}
synchronized void addOne(a) {
value += 1; }}Copy the code
If the addOne() method is synchronized, only one thread can execute the addOne() method on either a single-core CPU or a multi-core CPU. Therefore, atomic operation is guaranteed.
The rule for locking in a pipe is that the unlocking of a lock Happens-Before the locking of the lock Happens.
Synchronized. We know that synchronized modifiers are mutually exclusive, meaning that only one thread executes the code in the critical region at any one time. The so-called “happens-before unlocking a lock” refers to the unlocking action of the previous thread and the locking action of the next thread. The transitivity principle of happens-before is combined. We know that the shared variables that the previous thread modified in the critical section (before unlocking) are visible to subsequent threads that enter the critical section (after unlocking).
By this rule, visibility is guaranteed if multiple threads execute the addOne() method at the same time, meaning that if 1000 threads execute the addOne() method, the end result must be an increment of value by 1000. Seeing the result, we breathed a sigh of relief that the problem was finally solved.
But maybe you accidentally overlooked the get() method. Is the value of value visible to the get() method after the addOne() method is executed? This visibility is not guaranteed. The get() method does not lock the lock, so visibility is not guaranteed. So what’s the solution? Get () is synchronized. The complete code is shown below.
class SafeCalc {
long value = 0L;
synchronized long get(a) {
return value;
}
synchronized void addOne(a) {
value += 1; }}Copy the code
The above code translates to the lock model we mentioned, which looks like this. Both the get() and addOne() methods need access to the protected resource value, which is protected by the this lock. To access the critical sections get() and addOne(), a thread must first acquire the lock this, so that get() and addOne() are mutually exclusive.
This model is more like the management of tickets for football matches in the real world. One seat is only allowed to be used by one person, and this seat is the “protected resource”. The entrance of the stadium is the method in Java class, and the ticket is the “lock” used to protect resources.
Relationship between a lock and a protected resource
As we mentioned earlier, the relationship between protected resources and locks is very important. What is the relationship? A reasonable relationship is that the relationship between the protected resource and the lock is N:1. Also take the front of the game ticket management to analogy, is a seat, we can only use a ticket to protect, if there are multiple tickets, it will be a fight. In the real world, we can protect the same resource with multiple locks, but not in the concurrent world, where locks do not match the real world lock exactly. However, it is possible to use the same lock to protect multiple resources, which in the real world is what we call “farm”.
If I change the value to a static variable and addOne() to a static method, do I have concurrency problems with get() and addOne()?
class SafeCalc {
static long value = 0L;
synchronized long get(a) {
return value;
}
synchronized static void addOne(a) {
value += 1; }}Copy the code
If you look closely, you’ll see that the changed code protects one resource with two locks. The protected resource is the static variable value, and the two locks are this and safecalc.class. We can visualize this relationship in the following picture. Because critical sections get() and addOne() are protected by two locks, the two critical sections are not mutually exclusive, and changes to value by addOne() do not guarantee visibility to get(), which leads to concurrency problems.
conclusion
Mutex is very famous in the field of concurrency. As long as there is a concurrency problem, the first thing that comes to mind is locking, because we all know that locking can ensure the mutual exclusion of critical section code. This understanding is correct, but it does not guide you to truly use mutex. The code for a critical section is a path to a protected resource, similar to the entrance to a stadium, which must be ticketed, i.e. locked, but not every lock will work. Therefore, the relationship between the locked object and the protected resource must be thoroughly analyzed, and the access path of the protected resource must be taken into consideration in order to use the mutex properly.
Synchronized is a mutual-exclusion primitive provided by Java at the language level. There are many other types of locks in Java, but as a mutual-exclusion lock, the principles are the same: Lock, there must be an object to lock, the locked object to protect the resource and where to lock/unlock, is a design level matter.