This is the NTH day of my participation in Gwen Challenge
Hair a lot of programmer: "master, this batch processing interface is too slow, what method can optimize?"
Architect: "Try using multi-threaded optimization"
The second day
Hair a lot of programmer: "master, I already used multithread, why interface return slow?"
Architect: "Go buy me a cup of coffee and I'll write an article about it."
... I went to get some coffee
Copy the code
In practical work, the wrong use of multithreading can not only improve efficiency but also cause the program crash. Take driving on the road:
On a one-way road, where every car obeys the traffic rules, overall traffic is normal. “One-way lane” means “one thread” and “multiple cars” means “multiple job tasks”.
If you need to improve the efficiency of a vehicle, the general approach is to expand the lane, which is equivalent to the program “add thread pool”, increase the number of threads. So at the same time, the number of vehicles passing is far greater than a single lane.
However, the adult world is not so perfect, once more lane “jam” scene will be more and more, the occurrence of collisions will affect the efficiency of the whole road. By comparison, “multi-lane” may indeed be slower than “single-lane”.
Adding “barriers” between lanes could prevent frequent lane changes, but what about in the world of programming?
The problems encountered by multithreading in the program world can be summarized into three categories: “thread-safety problems”, “activity problems”, and “performance problems”. These problems and their corresponding solutions will be explained in the following sections.
Thread safety
Sometimes we find that code that works well in a single-threaded environment can have unexpected results in a multi-threaded environment, which is often referred to as “thread unsafe.” So what exactly is thread unsafe? To look down.
atomic
Take A bank transfer example, for example, transferring 1000 yuan from account A to account B, there must be two operations: subtract 1000 yuan from account A and add 1000 yuan to account B. Only when both operations are successful, the transfer will be successful.
Imagine if these two operations were not atomic, and after $1000 was deducted from account A, the operation suddenly stopped, and account B did not add $1000, that would be A big problem.
The example of bank transfer has two steps. The failure of the transfer after an accident shows that there is no atomicity.
Atomicity: An operation or operations are either all performed without interruption by any factor, or none at all.
Atomic operations: operations that are not interrupted by thread scheduling mechanisms, with no context switches.
Many operations in concurrent programming are not atomic operations. Here’s a quick question:
i = 0; / / 1
i++; / / 2
i = j; / / 3
i = i + 1; / / 4
Copy the code
Which of these four operations are atomic and which are not? The uninitiated might think that these are all atomic operations, but only operation 1 is atomic.
- Operation 1: Assignment to a primitive datatype variable is an atomic operation;
- Operation 2: contains three operations: read the value of I, add I by 1, and assign the value to I;
- Operation 3: Read j and assign j to I;
- Operation 4: contains three operations: read the value of I, add I by 1, and assign the value to I;
None of the above operations is a problem in a single-threaded environment, but in a multi-threaded environment it is often possible to get unexpected values without locking.
Atomicity can be ensured in the Java language by using Synchronize or lock.
visibility
Talk is cheap, show code:
/ * *
* Author: leixiaoshuai
* /
class Test {
int i = 50;
int j = 0;
public void update(a) {
Thread 1 executes
i = 100;
}
public int get(a) {
Thread 2 executes
j = i;
return j;
}
}
Copy the code
Thread 1 performs the update method to assign I to 100. Normally, thread 1 completes the assignment in its working memory, but does not flush the new value to main memory in time.
Thread 2 executes the get method, first reads the value of I from main memory, then loads it into its working memory, then reads the value of I 50, assigns 50 to j, and finally returns the value of J 50. It was expected to return 100, but it returned 50, so that’s the visibility problem. Thread 1 made changes to variable I, and thread 2 didn’t immediately see the new value of I.
Visibility: When multiple threads access the same variable, if one thread changes the value of the variable, the other threads can see the changed value immediately.
As shown in the figure above, each thread has its own working memory, and the working memory interacts with the main memory through store and load.
To address multithreaded visibility, the Java language provides the keyword volatile. When a shared variable is volatile, it guarantees that the value is immediately updated to main memory, and that it will read the new value in memory when another thread needs to read it. Common shared variables do not guarantee visibility because it is uncertain when a variable is flushed back to main memory after modification, and another thread may read the old value.
Of course, Java locking mechanisms such as Synchronize and Lock also ensure visibility. Locking ensures that only one thread executes a synchronized code block at a time, flushing variables back to main memory before releasing the lock.
Another aspect of thread insecurity is “orderliness,” which will be covered in more detail in a later article.
Activity problem
As mentioned above, locking can be used to solve visibility problems, but other problems can be introduced if locking is not used properly, such as “deadlocks”.
Before we say “deadlock,” let’s introduce another concept: the activity problem.
Activity means that something right will eventually happen, and activity problems occur when an operation can’t continue.
If you don’t understand the concept, remember that there are several types of activity problems: deadlocks, live locks, and starvation problems.
(1) Deadlock
A deadlock is when multiple threads block forever because of a circular waiting lock. A picture is worth a thousand words without much explanation.
(2) Live lock
A deadlock is a blockage caused by two threads waiting for each other to release the lock. A live lock means that the thread is not blocked and is still alive.
A live lock occurs when multiple threads are running and changing their state, and other threads depend on each other for this state, so that no one thread can continue to execute, but can only repeat its own action and change its state.
! [](/Users/ray/Library/Application Support/typora-user-images/image-20210408232019843.png)
If you are still confused, let me give you another example in life. When you are walking, a person comes towards you, and the two people give way to each other, but walk in the same direction at the same time. If you keep repeating the same way, the two people are locked alive.
(3) Hunger
If a thread cannot continue running without other exceptions, it is basically starving.
There are several common scenarios:
- High-priority threads are always running and consuming CPU, and all low-priority threads are always waiting;
- Some threads are permanently blocked in a state waiting to enter a synchronized block, while other threads can always access the synchronized block before it;
A classic hunger problem is the philosopher’s meal problem. As shown in the picture below, there are five philosophers at the table, each of whom must hold two forks at the same time before starting the meal. If Philosopher 1 and Philosopher 3 start eating at the same time, philosophers 2, 4 and 5 will have to wait without food.
Performance issues
We talked about thread safety and deadlocks and live locks that can affect multithreaded execution. If none of these things happen, is multithreaded concurrency necessarily faster than single-threaded serial execution? The answer is not necessarily, because multithreading has the overhead of creating threads and switching threads context.
Thread creation is directly to the system to apply for resources, for the operating system to create a thread is very expensive, need to allocate memory to it, included in the schedule, etc.
After a thread is created, it also encounters a thread context switch.
CPU is a very valuable resource is also very fast, in order to guarantee the rain of usually assign different threads a time slice, CPU from executing a thread to execute another thread, the need to save the current CPU thread local data, application of Pointers, and load the next to executing thread local data, application of Pointers, This switch is called context switching.
General methods to reduce context switching are: lockless concurrent programming, CAS algorithm, using coroutines, etc.
Summary with attitude
Multithreaded use can make the efficiency of the program doubled, bad use may be slower than a single thread.
To sum up the above, use a picture:
As you may have noticed, the article does not provide specific solutions to the problems encountered in multi-threaded concurrency, because these problems have been considered for you during the Java language design process.
Java concurrent programming can be difficult to learn, but it is also the path from a beginner programmer to an advanced programmer. The following articles will guide you through them one by one!
Author: Lei Xiaoshuai
Github “Java Eight Article” open source project author, focus on Java interview routine, Java advanced learning, break the inner book to get the Offer of big factory, promotion and salary increase!
About the author:
☕ read a few years: Huazhong University of Science and Technology master graduated;
😂 wave over several big factories: Huawei, netease, Baidu……
😘 has always believed that technology can change the world, is willing to maintain the original heart, refueling technicians!