Home  >  Article  >  Java  >  Analysis of volatile application examples of Java basics

Analysis of volatile application examples of Java basics

WBOY
WBOYforward
2023-05-28 11:23:391278browse

Analysis of volatile application examples of Java basics

Q: Please talk about your understanding of volatile?
Answer: volatile is a lightweight synchronization mechanism provided by the Java virtual machine. It has three characteristics:
1) Guaranteed visibility
2) Does not guarantee atomicity Sex
3)Prohibit instruction rearrangement

You just finished learning the basics of Java, if someone asks you what is volatile? If it has any function, I believe you must be very confused...
Maybe you don’t understand at all after reading the answer, what is a synchronization mechanism? What is visibility? What is atomicity? What is instruction reordering?

1. Volatile ensures visibility

1.1. What is the JMM model?

To understand what visibility is, you must first understand JMM.

JMM (Java Memory Model, Java Memory Model) itself is an abstract concept and does not really exist. It describes a set of rules or specifications. Through this set of specifications, the access methods of various variables in the program are determined. JMM's regulations on synchronization:
1) Before a thread is unlocked, the value of the shared variable must be refreshed back to the main memory;
2) Before the thread is locked, the latest value of the main memory must be read into its own working memory;
3) Locking and unlocking are the same lock;

Since the entity of the JVM running program is a thread, when each thread is created, the JMM will create a working memory (called stack space in some places) for it. , working memory is the private data area of ​​each thread.

The Java memory model stipulates that all variables are stored in main memory. Main memory is a shared memory area that can be accessed by all threads.

But the thread's operations on variables (reading, assigning, etc.) must be performed in the working memory. First, you need to copy the variables from main memory to working memory, perform operations, and then write them back to main memory.

After reading the above introduction to JMM, you may still be confused about its advantages. Let’s use a ticket selling system as an example:

1) As shown below, at this time the backend of the ticket selling system only There is 1 ticket left and has been read into the main memory: ticketNum=1.
2) At this time, there are multiple users on the network who are grabbing tickets, so there are multiple threads that are performing ticket buying services at the same time. Assume that there are 3 threads that have read the current number of tickets: ticketNum. =1, then you will buy a ticket.
3) Assume that thread 1 first seizes the CPU resources, buys the ticket first, and changes the value of ticketNum to 0 in its own working memory: ticketNum=0, and then writes it back to the main memory.

At this time, the user in thread 1 has already purchased the ticket, so thread 2 and thread 3 should not be able to continue buying tickets at this time, so the system needs to notify thread 2 and thread 3 that ticketNum is now equal to 0: ticketNum=0. If there is such a notification operation, you can understand it as having visibility.

Analysis of volatile application examples of Java basics

Through the above introduction and examples of JMM, we can briefly summarize it.

The visibility of the JMM memory model means that when multiple threads access a resource in the main memory, if a thread modifies the resource in its own working memory and writes it back to the main memory, then the JMM memory The model should notify other threads to re-obtain the latest resources to ensure the visibility of the latest resources.

1.2. Code verification for volatile guaranteed visibility

In Section 1.1, we basically understood the definition of visibility, and now we can use code to verify the definition. Practice has proven that using volatile can indeed ensure visibility.

1.2.1. Non-visibility code verification

First verify whether there is no visibility if volatile is not used.

package com.koping.test;import java.util.concurrent.TimeUnit;class MyData{
    int number = 0;

    public void add10() {
        this.number += 10;
    }}public class VolatileVisibilityDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动一个线程修改myData的number,将number的值加10
        new Thread(
                () -> {
                    System.out.println("线程" + Thread.currentThread().getName()+"\t 正在执行");
                    try{
                        TimeUnit.SECONDS.sleep(3);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    myData.add10();
                    System.out.println("线程" + Thread.currentThread().getName()+"\t 更新后,number的值为" + myData.number);
                }
        ).start();

        // 看一下主线程能否保持可见性
        while (myData.number == 0) {
            // 当上面的线程将number加10后,如果有可见性的话,那么就会跳出循环;
            // 如果没有可见性的话,就会一直在循环里执行
        }

        System.out.println("具有可见性!");
    }}

The running result is as shown below. You can see that although thread 0 has changed the value of number to 10, the main thread is still in the loop because number is not visible at this time and the system will not actively notify.
Analysis of volatile application examples of Java basics

1.2.1. Volatile guarantee visibility verification

In line 7 of the above code, add volatile to the variable number and test again, as shown below, at this time the main thread The loop was successfully exited because JMM actively notified the main thread to update the value of number, and number is no longer 0.
Analysis of volatile application examples of Java basics

2. Volatile does not guarantee atomicity

2.1 What is atomicity?

After understanding the visibility mentioned above, let’s understand what atomicity is?

Atomicity refers to the characteristic that cannot be divided or interrupted and maintains integrity. In other words, when a thread is performing an operation, it cannot be interrupted by any factor. Either succeed at the same time or fail at the same time.

It’s still a bit abstract, let’s give an example.

As shown below, a class for testing atomicity is created: TestPragma. The compiled code shows that the increase of n in the add method is completed through three instructions.

因此可能存在线程1正在执行第1个指令,紧接着线程2也正在执行第1个指令,这样当线程1和线程2都执行完3个指令之后,很容易理解,此时n的值只加了1,而实际是有2个线程加了2次,因此这种情况就是不保证原子性。
Analysis of volatile application examples of Java basics

2.2 不保证原子性的代码验证

在2.1中已经进行了举例,可能存在2个线程执行n++的操作,但是最终n的值却只加了1的情况,接下来对这种情况再用代码进行演示下。

首先给MyData类添加一个add方法

package com.koping.test;class MyData {
    volatile int number = 0;

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

然后创建测试原子性的类:TestPragmaDemo。验证number的值是否为20000,需要测试通过20个线程分别对其加1000次后的结果。

package com.koping.test;public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);

    }}

运行结果如下图,最终number的值仅为18410。
可以看到即使加了volatile,依然不保证有原子性。
Analysis of volatile application examples of Java basics

2.3 volatile不保证原子性的解决方法

上面介绍并证明了volatile不保证原子性,那如果希望保证原子性,怎么办呢?以下提供了2种方法

2.3.1 方法1:使用synchronized

方法1是在add方法上添加synchronized,这样每次只有1个线程能执行add方法。

结果如下图,最终确实可以使number的值为20000,保证了原子性。

但在实际业务逻辑方法中,很少只有一个类似于number++的单行代码,通常会包含其他n行代码逻辑。现在为了保证number的值是20000,就把整个方法都加锁了(其实另外那n行代码,完全可以由多线程同时执行的)。所以就优点杀鸡用牛刀,高射炮打蚊子,小题大做了。

package com.koping.test;class MyData {
    volatile int number = 0;

    public synchronized void add() {
      // 在n++上面可能还有n行代码进行逻辑处理
        number++;
    }}

Analysis of volatile application examples of Java basics

2.3.2 方法1:使用JUC包下的AtomicInteger

给MyData新曾一个原子整型类型的变量num,初始值为0。

package com.koping.test;import java.util.concurrent.atomic.AtomicInteger;class MyData {
    volatile int number = 0;

    volatile AtomicInteger num = new AtomicInteger();

    public void add() {
        // 在n++上面可能还有n行代码进行逻辑处理
        number++;
        num.getAndIncrement();
    }}

让num也同步加20000次。可以将原句重写为:使用原子整型num可以确保原子性,如下图所示:在执行number++时不会发生竞态条件。

package com.koping.test;public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
        System.out.println("num值加了20000次,此时number的实际值是:" + myData.num);

    }}

Analysis of volatile application examples of Java basics

3、volatile禁止指令重排

3.1 什么是指令重排?

在第2节中理解了什么是原子性,现在要理解下什么是指令重排?

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排:
源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令

处理器在进行重排时,必须要考虑指令之间的数据依赖性。

单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。

但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

看了上面的文字性表达,然后看一个很简单的例子。
比如下面的mySort方法,在系统指令重排后,可能存在以下3种语句的执行情况:
1)1234
2)2134
3)1324
以上这3种重排结果,对最后程序的结果都不会有影响,也考虑了指令之间的数据依赖性。

public void mySort() {
    int x = 1;  // 语句1
    int y = 2;  // 语句2
    x = x + 3;  // 语句3
    y = x * x;  // 语句4}

3.2 单线程单例模式

看完指令重排的简单介绍后,然后来看下单例模式的代码。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

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

    public static void main(String[] args) {
        // 单线程测试
        System.out.println("单线程的情况测试开始");
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println("单线程的情况测试结束\n");
    }}

首先是在单线程情况下进行测试,结果如下图。可以看到,构造方法只执行了一次,是没有问题的。
Analysis of volatile application examples of Java basics

3.3 多线程单例模式

接下来在多线程情况下进行测试,代码如下。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }

        // DCL(Double Check Lock双端检索机制)//        if (instance == null) {//            synchronized (SingletonDemo.class) {//                if (instance == null) {//                    instance = new SingletonDemo();//                }//            }//        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试//        System.out.println("单线程的情况测试开始");//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }}

在多线程情况下的运行结果如下图。可以看到,多线程情况下,出现了构造方法执行了2次的情况。
Analysis of volatile application examples of Java basics

3.4 多线程单例模式改进:DCL

在3.3中的多线程单里模式下,构造方法执行了两次,因此需要进行改进,这里使用双端检锁机制:Double Check Lock, DCL。即加锁之前和之后都进行检查。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {//        if (instance == null) {//            instance = new SingletonDemo();//        }

        // DCL(Double Check Lock双端检锁机制)
        if (instance == null) {  // a行
            synchronized (SingletonDemo.class) {
                if (instance == null) {  // b行
                    instance = new SingletonDemo();  // c行
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试//        System.out.println("单线程的情况测试开始");//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }}

在多次运行后,可以看到,在多线程情况下,此时构造方法也只执行1次了。
Analysis of volatile application examples of Java basics

3.5 Multi-threaded singleton mode improvement, problems in the DCL version

It should be noted that the singleton mode of the DCL version in 3.4 is still not 100% accurate! ! !

Don’t you quite understand why the 3.4DCL version of the singleton mode is not 100% accurate?
Don’t you quite understand why we suddenly need to talk about the multi-threaded singleton mode after finishing the simple understanding of instruction rearrangement in 3.1?

Because the 3.4DCL version of the singleton mode may cause problems due to instruction rearrangement, although the possibility of this problem may be one in ten million, the code is still not 100% accurate. If you want to ensure 100% accuracy, you need to add the volatile keyword. Adding volatile can prohibit instruction rearrangement.

Let’s analyze next, why is the 3.4DCL version of the singleton mode not 100% accurate?

View instance = new SingletonDemo(); the compiled instructions can be divided into the following three steps:
1) Allocate object memory space: memory = allocate();
2) Initialize the object: instance(memory);
3) Set instance to point to the allocated memory address: instance = memory;

Since there is no data dependency between steps 2 and 3, step 132 may be executed.
For example, thread 1 has executed step 13 but has not executed step 2. At this time instance!=null, but the object has not been initialized yet;
If thread 2 preempts the CPU at this time and then finds instance!=null, Then directly return to use, you will find that the instance is empty, and an exception will occur.

This is a problem that may be caused by instruction rearrangement. Therefore, if you want to ensure that the program is 100% correct, you need to add volatile to prohibit instruction rearrangement.

3.6 The principle of volatile guaranteeing to prohibit instruction rearrangement

In 3.1, we briefly introduce the meaning of execution rearrangement, and then through 3.2-3.5, we use the singleton mode to illustrate the multi-thread situation. Next, the reason why volatile should be used is because there may be instruction rearrangement that causes program exceptions.

Next, we will introduce the principle that volatile can ensure that instruction reordering is prohibited.

First we need to understand a concept: Memory Barrier, also known as memory barrier. It is a CPU instruction with two functions:
1) Guarantee the execution order of specific operations;
2) Guarantee the memory visibility of certain variables;

Since both the compiler and the processor Able to perform command rearrangement. If a Memory Barrier is inserted between instructions, it will tell the compiler and CPU that no instructions can be reordered with this Memory Barrier instruction. In other words, prohibits instructions before and after the memory barrier by inserting a memory barrier. Execution reordering requires optimization.

Another function of the memory barrier is to force the cache data of various CPUs to be flushed, so any thread on the CPU can read the latest version of these data.

Analysis of volatile application examples of Java basics

The above is the detailed content of Analysis of volatile application examples of Java basics. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete