1. Concurrency issues
The reason for concurrency is simply that multiple threads competing for the same shared resource can produce results that are inconsistent with those expected under a single thread. This is technically known as a “thread-safe problem.”
E.x: the simplest example is our i++ pull, which is almost all concurrent programming to discuss the problem.
private static long count = 0;
private static void add10K(a) {
int idx = 0;
while (idx++ < 10000) {
count += 1;
// Open this comment, there is no thread safety issue, you can use your IDE copy code to run
// This question is interesting and worth thinking about
// System.out.println(count);}}public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run(a) { IDemo.add10K(); }}); Thread thread2 =new Thread(new Runnable() {
@Override
public void run(a) { IDemo.add10K(); }}); thread.start(); thread2.start();// Sleep for 2S before query count
Thread.sleep(2000);
System.out.println(count);
}
Copy the code
Conclusion: You will find that the result is not 20000, but a random number of 10000-20000.
In fact, this is the thread safety problem caused by the concurrent looting of resources caused by multiple threads.
2. Why does this thread safety problem occur?
I’ve been asked this question a lot as I’ve been interviewing companies, but at each stage, I’ve had a different experience. And it’s really easy to answer, three properties, visibility, atomicity, and orderliness
There’s no need to memorize the exact definition of each feature.
We can talk about the underlying implementation, and we can talk about the three main features from the perspective of the underlying computer, and then we can deduce why this “thread safety” problem occurs.
2.1 Three Features
- visibility
Our computers have evolved from single-core to multi-core, which means the number of cpus we have.
What does the CPU do, the CPU is to execute the instructions that we write one by one, so called multiple cpus, simply put, by the number of cpus in the heap, improve the computing power of the computer
Let’s talk about the STRUCTURE of CPU, development to the present, CPU in order to improve the efficiency of execution, will have a cache of its own, so that it does not have to go to the memory to obtain data every time, improve the efficiency of obtaining data.
Therefore, the CPU will have its own CPU cache area, divided into three parts, L1,L2,L3 (these three are the CPU cache, the higher the number, the greater the storage space), so, I use a diagram to briefly represent the internal structure of the CPU.
So how many cpus do we have? In fact, it is very simple, in fact, each CPU read from the main memory cache into its own CPU dedicated cache
So with that knowledge, visibility, which is actually between caches between cpus, is not visible, and that can cause problems.
For example, if the variable I is equal to 0 in main memory, then CPU0 reads the variable I into CPU memory, and executes the I ++ operation. At this point, CPU0’s cache contains I =1, and CPU1 has not flushed I =1 into main memory, so CPU1 has to perform an I ++ operation. From main memory read at this moment I = 0, i++, with the old values, using the i++ of dirty data for operation, so not a problem?
- atomic
The idea of atomicity is that an action is indivisible, and you have to wait until this action is done before you can do the next one. We either succeed together or we fail together
In Java, the following operations are defined to be atomic (indivisible) and not recommended to be memorized directly, but to be understood in conjunction with semantics.
Lock: Acts on variables in main memory to identify a variable as a thread-exclusive state; Unlock: a variable used in main memory that transfers the value of a variable from main memory to the working memory of the thread for later load action. 4. Load acts on a variable in working memory, which puts the value of the variable from main memory into a copy of the variable in working memory from the read operation: Function on a variable in working memory, which passes the value of a variable in working memory to the execution engine. This operation is performed whenever the virtual machine reaches a bytecode instruction that needs to use the value of the variable. Assign: applies to a variable in working memory. It assigns a value received from the execution engine to a variable in working memory. This operation is performed whenever the virtual machine accesses a bytecode instruction that assigns a value to the variable. Store: a variable applied to working memory that passes the value of a variable in working memory to main memory for subsequent write operations; Write (operation) : variable acting on main memory, which puts the value of the variable obtained from the working memory by the store operation into the main memory variableCopy the code
Let me use a picture to summarize the following text (Suggest drawing once, deepen impression) :
So, you must be wondering how the operating system guarantees atomicity in the eight steps above.
In fact, the operating system guarantees atomicity by means of bus locking or cache locking.
When a processor issues a #Lock signal, all requests from other processors are blocked. The processor can monopolizes shared memory resources until the processor issues a #Unlock signal.
Bus locking is safe, but very inefficient, because it is the LOCK between CPU and memory, during the lock, even other threads can not operate on other memory, extremely inefficient. Therefore, cache locking should be implemented accordingly.
Cache Lock, in fact, frequently used data will be loaded into the L1,L2,L3 cache, if we want to change this part of the data, the CPU in the #Lock will directly modify the memory address of the cache data, and will prohibit other CPUS to change the memory address of this part of the cache data. In order to ensure atomicity.
- order