Understanding of volatile and code demonstration

    Volatile is a lightweight synchronization mechanism provided by java virtual machine. It has three major features: ensuring visibility, not atomicity, and prohibiting instruction rearrangement. Before you want to understand volatile, you must first understand the running principle of JVM and JMM, which is java memory model. Because the real body of JVM running program is thread, the JVM will create a working memory for each thread when it is created For stack space), working memory is the private data area of each thread, while java Memory Model stipulates that all variables are stored in the main memory, which is the shared memory area and can be accessed by all threads, but the operation (reading and assigning) of variables by threads must be carried out in working memory. First, variables should be copied from the main memory to their own working memory space, and then To operate variables, write the variables back to the main memory after the operation is completed. The variables in the main memory cannot be operated directly. The memory of each thread stores a copy of the variables in the main memory, so different threads cannot access each other's working memory. The communication (value transmission) between threads must be completed through the main memory.

    For example, to illustrate, suppose the JVM has opened three threads for us to execute A java program at the same time and assign A value to variable A. the JVM will store variable A in the main memory. The initial value is 0. At this time, there are three threads executing at the same time. The first three threads copy the object variable A from the main memory to their own working memory and modify it by their own threads. Suppose thread 2 is better than thread 2 His thread runs fast. After assigning A value to variable A, he copies it back to the main thread. At this time, thread 1 and thread 3 still hold the value of variable A before being modified by thread 2, which will cause data inconsistency. If thread 2 modifies the value of variable A and informs other line variables of the changed action, it is the visibility of variables.

    Next, we use a chestnut to see the different results of variables without volatile and with volatile decoration. We define a resource class, and define a number variable, which is initialized to 0. In defining a method to change the value of this variable, we start a new thread to call the method to change the number. The main thread waits or knots according to the value of the variable Bundle.


public class VolatileDemo {

    public static void main(String[] args) {

        MyData myData = new MyData();

        new Thread(() -> {

            System.err.println(Thread.currentThread().getName() + "\t come in");
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            myData.addTo60();

            System.err.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
        }, "Thread 1").start();

        while (myData.number == 0) {

        }

        System.err.println("end");
    }

}

class MyData {

    int number = 0;

    public void addTo60() {
        this.number = 60;
    }
}
 

Execute the result, and the program is running all the time without outputting the last end

Thread 1     come in
Thread 1     update number value:60

This is what my friends are wondering. I have changed the value of number to 60. Why is the program still not finished? This is what we mentioned above. Although the variables in main memory have been modified, but the main thread has not been informed. This is to realize the role of volatile. It is to notify other threads after the variables in main memory have been modified. Let's take an example of variable plus volatile


public class VolatileDemo {

    public static void main(String[] args) {

        MyData myData = new MyData();

        new Thread(() -> {

            System.err.println(Thread.currentThread().getName() + "\t come in");
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            myData.addTo60();

            System.err.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
        }, "Thread 1").start();

        while (myData.number == 0) {

        }

        System.err.println("end");
    }

}

class MyData {

    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }
}
 

From the execution result, the last end is output, indicating that the latest value of the variable is visible to all threads.

Thread 1     come in
Thread 1     update number value:60
end

    Next let's look at atomicity. What is atomicity? Indivisibility and integrity? When a thread is doing a specific business, it can't be plugged or divided in the middle. It needs to be complete as a whole, successful or failed. As mentioned above, volatile doesn't guarantee atomicity. We are looking at a chestnut, which is modified by volatile Variable, we start 20 threads to add variable modified by volatile 1000 times to see if the final result is 20000.


public class VolatileDemo {

    public static void main(String[] args) {

        MyData myData = new MyData();

        for (int i = 0; i < 20; i++) {

            new Thread(() -> {

                for (int j = 0; j<1000; j++) {
                    myData.increment();
                }

            }, "Thread " + String.valueOf(i)).start();
        }

        System.err.println("myData.number=" + myData.number);

        System.err.println("end");
    }

}

class MyData {

    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    public void increment() {
        number++;
    }
}

We execute it several times more, and the result is not the same every time, and it's not up to 20000, which means that volatile doesn't guarantee atomicity

myData.number=18917
end

myData.number=17974
end

myData.number=18887
end

At this time, some partners will say, is it that if 20 threads are not finished, the program will end? We are adding a bold code to control the end of 20 threads, and the final number value is less than 20000.

public class VolatileDemo {

    public static void main(String[] args) {

        MyData myData = new MyData();

        for (int i = 0; i < 20; i++) {

            new Thread(() -> {

                for (int j = 0; j < 1000; j++) {
                    myData.increment();
                }

            }, "Thread " + String.valueOf(i)).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.err.println("myData.number=" + myData.number);

        System.err.println("end");
    }

}

class MyData {

    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    public void increment() {
        number++;
    }
}
 

    From the running results, the value of the variable number is still less than 20000. Explain the bold code, that is, when the current number of threads is greater than 2, they have been waiting. One of the two threads is the main thread. What is the reason and why this phenomenon occurs? At the beginning of the article, we briefly talked about the java memory model. When the JVM is started, the number will be changed Variables are put into the main memory. The 20 threads opened here copy the number variable to their respective working memory for addition. When the addition is finished, the number variable is copied back to the main memory. Because the computer runs very fast, and when the value is written to the main memory, only one thread can write, and the write coverage will appear. For example, threads 1 to 2 The thread of 0 gets the value of number variable at the same time, both of which are 0, and each of them adds 1. When thread 1 is relatively fast, add 1 to the value of number continuously, and other threads are waiting. When the value of number becomes 3, thread 2 gets the permission to write number. The reason why number doesn't become 20000 in the end is clear here.

So how to solve the atomicity problem? The first one is to add synchronized to the method, that is, add number + + to the method in the upper resource class code

class MyData {

    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    public synchronized void increment() {
        number++;
    }
}

In this way, after running the main method, it will be 20000 each time, because of the synchronized This method is locked. Here is just variable addition. If there are other logics, every thread will block there and wait for the method to finish executing. That's obviously not a good solution. Let's take a look at other solutions to atomicity. That's the original solution under the concurrent package (commonly known as JUC package) of jdk Subclasses, such as AtomicInteger, AtomicLong, AtomicBoolean, etc. under the package of JUC, provide atomic classes for basic types int, long, boolean, and the atomic class AtomicReference referenced by objects. Let's use a piece of code to see the number + + implemented by atomic classes.

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileDemo {

    public static void main(String[] args) {

        MyData myData = new MyData();

        for (int i = 0; i < 20; i++) {

            new Thread(() -> {

                for (int j = 0; j < 1000; j++) {
                    myData.atomicIncrement();
                }

            }, "Thread " + String.valueOf(i)).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.err.println("myData.ai=" + myData.ai);

        System.err.println("end");
    }

}

class MyData {

    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    public synchronized void increment() {
        number++;
    }

    public AtomicInteger ai = new AtomicInteger();

    public void atomicIncrement() {
        ai.getAndIncrement();
    }
}
 

From the results of each execution, it is still 20000, indicating that the atomic classes under the JUC package also solve the atomicity problem.

Next, let's talk about volatile's prohibition of instruction rearrangement. The java code we write will eventually be compiled into machine code that can be recognized by the machine, also known as instruction. In order to improve the performance of the program when the computer executes, the compiler and processor often rearrange the instruction. Let's take a look at a chestnut.

public class ReSortSeqDemo {

    int a = 0;
    boolean flag = false;

    public void method1() {

        a = 1;
        flag = true;

    }

    public void method2() {

        if (flag) {
            a = a + 5;
            System.err.println("a = " + a);
        }

    }

}
 

In method2, when flag is true, add a to 5, and print. Because there is instruction rearrangement in the computer, in method1, after instruction rearrangement, it is possible that flag=true will be executed first, and then a=1, so that the value of a will be printed finally, and the result will be different each time.

Where is volatile used? Classic is double check lock (DCL) when creating a single instance object. This mechanism is not thread safe, because there is instruction rearrangement. Adding volatile can prevent instruction rearrangement. Let's see the chestnut of single instance with volatile modified DCL.

public class SingletonDemo {

    private static volatile SingletonDemo instance = null;

    public SingletonDemo() {
        System.err.println(Thread.currentThread().getName() + ":create intance");
    }

    public static synchronized SingletonDemo getInstance() {
        if (null == instance) {
            synchronized (SingletonDemo.class) {
                if (null == instance) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {

            new Thread(() -> {

                SingletonDemo.getInstance();

            }, "Thread " + String.valueOf(i)).start();
        }
    }

}
 

This is the end of the explanation of volatile. Now as long as it is an internet project, it must involve high concurrency, solve the problem of high concurrency, and there must be data inconsistency, etc., so we must understand the java high concurrency technology. See next article.

Tags: Java jvm less JDK

Posted on Sun, 21 Jun 2020 06:17:13 -0400 by nealios