Java takes you to learn the volatile keyword (thread atomicity, visibility, orderliness)

Let's not talk about this keyword, let me ask you a question first, you know the thread:

Visibility

Atomicity

Orderliness   

 

Are these three special features?

 

If you don’t understand, just continue to read below

 

When each thread in Java is working, it has its own unique working memory. This working memory is accessed by the thread alone and cannot be accessed by other threads.

When each thread reads and writes data, it first copies the data in the main memory to its own working memory, and then when the thread needs to read and write to modify the data, it will first read and write in its own working memory. Then flush the data to the main memory. But when will it be flushed to main memory? nobody can tell you...

Because there are too many factors involved

 

==============[ Visibility ]==============

There is a phenomenon at this time, such as:

The main memory has an attribute isExit,

Thread 1 reads the copy to its own working memory,

Thread 2 also copies the data of the variable isExit to its working memory and changes the value of the variable isExit.

For example, the following code:

    private boolean isExit = false;

    public void testVolati(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isExit) {
                    //do something
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                isExit = true;
                //do something
            }
        }, "线程2").start();
    }

If this isExit does not require real-time response, this is fine. If it needs to modify its value, other threads must know immediately and must refresh the latest value, there is a problem.

what? ? ? ? why?

 

Because thread 2 modifies the value of isExit, it is uncertain when thread 2 will flush to main memory.

If thread 1 has already copied isExit (ie isExit = false) to its own working memory before thread 2 modifies the value, thread 1 will not go to main memory

Read this value. So the value in thread 1 is always isExit = false. So the execution function of thread 1 has been running. Although thread 2 has changed the value of isExit.

 

This is the thread of invisibility  , a thread changes the value of other threads may not immediately known

 

In order to solve this problem, Java provides   a keyword volatile  . As long as the attribute modified by  volatile  is modified, all threads will immediately learn about it and read it again in the main memory, and then refresh it to its own working memory. For example, modify the above code to use volatile  for isExit  

    private volatile boolean isExit = false;

    public void testVolati(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isExit) {
                    //do something
                }
            }
        }, "线程1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                isExit = true;
                //do something
            }
        }, "线程2").start();
    }

This  modifies isExit through volatile  . Once thread 2 changes the value of isExit, thread 1 will immediately learn about it, read it from main memory and refresh it to isExit in its own working memory. Therefore,    the variable modified    by   volatile has  thread visibility

==============[ atomicity ]==============

Well, let’s take a look at what atomicity is. Let’s not talk about it for now. Let’s take a look at the previous code.

    private volatile int num = 0;//累加的变量
    private int count = 0;//完成线程累加
    private int THREAD_COUNT = 20;//线程数目

    public void textVo() {
        //开启20个线程
        for (int x = 0; x < THREAD_COUNT; x++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        num++;
                    }
                    threadOK();
                }
            });
            thread.start();
        }

    }

    private void threadOK() {
        count++;
        //等待所有线程都执行完累加
        if (count == THREAD_COUNT) {
            Log.d(TAG, "num : " + num);
        }
    }

 

Looking at the code, it is very simple. Start 20 threads, and each thread adds 10000 to the variable num. According to the truth, 20*10000, the result should be: 200000

Okay, let's show you the output:

 

The probability is less than the number we expect, what? ? ? ? ? you kidding me ????

Why is it like this? Come, let me analyze for you:

In fact, the problem appears in the line of num++. We all know that the Java language is finally compiled into assembly language, and then operated by assembly instructions.

The num++ line of code is no exception, it will eventually be compiled into N assembly instructions for execution, and probably decompiled assembly instructions:

 

		Code:
		Stack=2,Locals=0,Args_size=0,
		0:	getstatic  #98;//
		3:	iconsts_1
		4:  iadd
		5:  putstatic  #98
		8:  return

 

The whole process is probably in the assembly instructions:

 

1: First read out the data and put this value at the top of the operation stack (line 0)

2: Accumulate data (line 4)

3: Return the accumulated data (line 8)

 

 

Then there will be a problem at this time,

such as:

1: My thread 1 will read the value of num equal to 100, and then put this value on the top of the operation stack. At this time, the CPU execution power is taken away by other threads

2: Thread 2 grabs the execution right of the CPU at this time, and then reads the value of num equal to 100, and returns 101 to num after accumulation. At this time, num = 101

3: Then this time the CPU execution right is snatched back by thread 1. Note that the value at the top of the stack of thread 1 is still 100, and then the value class is added to get 101, and the value is returned to num

4: Then the problem is very clear. Thread 1 and Thread 2 are accumulated once, and the final value is only increased by 1.

 

But,,,, you may see that num is     modified by volatile  , and after line 2 is modified, why doesn't thread 1 know immediately and go to main memory to read new data? Doesn’t it mean that you have thread visibility?

 

It is good for you to have this question, but it is cruel to tell you that the assembly instruction puts the value reading on the top of the operation stack, and how you manipulate the variable num in other threads has nothing to do with it. The subsequent accumulation operation instructions are all the data on the top of the original stack.

 

So, this is the atomicity of threads   

 

Atomicity, that virtual machine when executing a Java instruction, we can guarantee a thread    must perform complete  a Java instructions   before you can perform right away by another thread CPU. Note that it is a Java instruction  ,

For example, a simple line of java code like num++,

Obviously,   variables modified by the volatile  keyword are not available, which means that num variables do not have the atomicity of threads.

Because it even has a num++, such a simple auto-increment instruction does not guarantee that it will be executed by other threads before it is executed.

In fact, from another perspective, it can be considered that the computing function of threads cannot guarantee atomicity.

 

==============[ Orderliness ]==============

Okay, finally, let me talk about orderliness.

Let's talk about the behavior of executing instructions of the Java virtual machine. Let's talk about the last Demo

    int a = 0;//第1行
    int b = 1;//第2行
    int c = 2;//第3行
    private void test() {
        a++;      //第4行
        b++;      //第5行
        c = a + b;//第6行
    }

It's just a few lines of code. Virtual machine is not guaranteed

Line 4 will definitely be executed before line 5. After the virtual machine is compiled, it will re-optimize and sort Java.

Only guarantee: when the execution reaches the 6th line of code, the 4th and 5th lines must have been executed.

In other words, only guarantee that the final result output is correct. There is no guarantee that the execution order is consistent with what you wrote.

Well, let me see what will happen to DoubleLock singleton mode without  volatile  modification

 

Now, the last singleton mode without  volatile  , the standard DoubleLock singleton mode,

public class ThreadDemo {

    private static ThreadDemo threadDemo;

    public ThreadDemo getInstance() {
        if (threadDemo == null) {
            synchronized (ThreadDemo.this) {
                if (threadDemo == null) {
                    threadDemo = new ThreadDemo();//第9行代码
                }
            }
        }
        return threadDemo;
    }

}

If you don’t add volatile   , there will be thread safety issues, why not add it?

In fact, thread safety will appear in the 9th line of code. In fact, the 9th line of code is divided into three steps to execute by default.

1 : Allocate a memory space in the heap memory to an instance of ThreadDemo, (that is, new ThreadDemo() )

2 : Initialize the memory instance ( call the ThreadDemo constructor to initialize the instance)

3 : Create a threadDemo variable in the internal memory and point to the address created in step 1 ( as long as this step is executed, threadDemo is not empty )

 

Obviously, a single  line of threadDemo = new ThreadDemo()   is divided into 3 steps.

More importantly, as just said, even in the default order, the compiler may optimize the code execution order.

The order after optimizing the code may be: 1-2-3   or it may be: 1-3-2

1-2-3 will be fine by then.

 

Let's take a look at what problems will happen if the compiler changes the execution order to 1-3-2 .

I construct a scene:

Thread 1 comes first, and executes the 9th line of code first. After executing 1-3, threadDemo is already non-empty. At this time, the CPU execution power is taken away by thread 2

At this time, thread 2 obtains the CPU execution right, comes in to obtain a singleton, judges that threadDemo is not equal to empty, and directly takes it away and uses it.

 

This is a problem. Because at this time: threadDemo simply points to a memory address, but the data stored in this address has not yet been initialized. There will be many unforeseen problems with this example, such as null pointers, etc..........

 

So in order to solve this problem, Java gives the keyword volatile   . Variables modified with this keyword tell the compiler:

The code before this variable must be executed by the compiler first.

The code after this variable must be executed after the compiler has guaranteed.

hard to pronounce? Is it hard to understand?

We write a few lines of code:

volatile int a = 1;
int b = 1;
int c = 1;
int b = 1;

private void test(){
    b++;//第7行
    c++;//第8行

    a++;//第9行

    b = c+b;//第11行
    c = b+;10//第12行

}

 

The variable a is modified with  volatile   , and the compiler guarantees

The code on lines 7 and 8 must be executed before line 9

The code on lines 11 and 12 must be executed after line 9

But it is not guaranteed which line will be executed first in line 7 and line 8.

Of course, there is no guarantee which line of line 11 and line 12 will be executed first.

 

The volatile   modified variable is equivalent to a dividing line. The preceding code must be executed before itself, and the following must be executed after itself

 

Okay, let’s go back to the single example mode just now. If we modify the threadDemo variable with  volatile   , we can ensure that steps 1 and 2 must be executed before step 3, so the order after compilation must be: 1-2-3, So after threadDemo points to the memory address, the data of the heap memory address must have been initialized. So there will be no thread safety issues.

 

So the correct DoubleLock singleton mode should look like this:

public class ThreadDemo {

    private volatile static ThreadDemo threadDemo;

    public ThreadDemo getInstance() {
        if (threadDemo == null) {
            synchronized (ThreadDemo.this) {
                if (threadDemo == null) {
                    threadDemo = new ThreadDemo();//第9行代码
                }
            }
        }
        return threadDemo;
    }

}

 

So we summarize:

Variables modified with  volatile   ,

No thread atomicity

    Thread visibility

    Has thread order

 

 

There is no problem with the above code, please leave a message for correction. . . Thank you

 

 

 

 

 

 

 

 

 

 

 

 

 

Guess you like

Origin blog.csdn.net/Leo_Liang_jie/article/details/91465477