Singleton mode and Volatile learning

Singleton mode and Volatile learning

Beep beep link: https://www.bilibili.com/video/BV15b4y117RJ?p=56

requirement

  • Master the implementation methods of five singleton modes
  • Understand why you should use volatile to modify static variables when implementing DCL (double check lock)
  • Understand the scenario of using singleton in jdk

1, Five implementation methods

1. Hungry Han style

Class is created as soon as it is initialized

Implementation requirements:

1. Construct private, which is required for the implementation of all singletons, because if it is not private, other classes have the opportunity to call the construction method to create instance objects, which will lead to multiple instances

2. Provide a static member variable. The member variable type is the singleton type, and the value is the only instance created with private structure

3. Static variables are generally private, and a public static method is generally provided to return static member variables

package com.singleton.test;

import java.io.Serializable;

// 1. Hungry Han style
public class Singleton1 implements Serializable {
    // Requirement 1: construct private
    private Singleton1() { System.out.println("private Singleton1()"); }

    // Requirement 2: provide a static member variable
    private static final Singleton1 INSTANCE = new Singleton1();

    // Requirement 3: static variables are generally private, and a public static method is generally provided to return static member variables
    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    // The intention of providing this static method: when other classes call this method, it will trigger the loading initialization of Singleton1 class, which will lead to the creation of singleton object for testing
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

Write test code,

package com.singleton.test;

public class Test {

    @org.junit.Test
    public void test1(){
        // The purpose of calling this static method is to trigger the loading initialization of Singleton1 class, which will cause the singleton object Singleton1 to be created
        Singleton1.otherMethod();
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());
    }
}

It is found that the same object is used,

Single case of reflection destruction

We use reflection breaking singleton mode,

private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    Constructor<?> constructor = clazz.getDeclaredConstructor(); // The parameter free construction method is obtained
    constructor.setAccessible(true); // Violent reflection, private construction methods can also be used
    System.out.println("Create instance of reflection:" + constructor.newInstance()); // Create instance
}

@org.junit.Test
public void test2() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    // The purpose of calling this static method is to trigger the loading initialization of Singleton1 class, which will cause the singleton object Singleton1 to be created
    Singleton1.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton1.getInstance());
    System.out.println(Singleton1.getInstance());

    // Single case of reflection destruction
    reflection(Singleton1.class);
}

It is found that a new object is created by calling the construction method again through reflection,

Prevent reflection breaking singleton mode: modify the construction method. If the object has been created, it cannot be created again

Test again and find that the object can no longer be created

Deserialization destruction singleton

We destroy the singleton pattern by deserialization. Note that the premise is that this singleton needs to implement the serialization interface

Supplement: serialization interface is required for storage on disk and network transmission

The test code is as follows:

private static void serializable(Object instance) throws IOException, ClassNotFoundException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(instance); // Turns the object into a byte stream
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
    // When the byte stream is restored to an object, deserialization will create a new object, and the deserialization does not go through the construction method
    System.out.println("Deserialize create instance:" + ois.readObject());
}

@org.junit.Test
public void test3() throws IOException, ClassNotFoundException {
    // The purpose of calling this static method is to trigger the loading initialization of Singleton1 class, which will cause the singleton object Singleton1 to be created
    Singleton1.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton1.getInstance());
    System.out.println(Singleton1.getInstance());

    // Deserialization destruction singleton
    serializable(Singleton1.class);
}

It is found that deserialization will create a new object, and the deserialization does not go through the construction method

Preventing deserialization corruption

We need to write a special readResolve method in the singleton class, which can return the singleton object.

Principle: if the readResolve method is overridden during deserialization, the returned value of this method will be used as the result, and the generated object will not be deserialized with byte array, which ensures the singleton mode.

After testing again, it is found that the deserialization returns our original singleton object

Single case of Unsafe failure

private static void unsafe(Class<?> clazz) throws InstantiationException {
    /*
     Unsafe It is a built-in class in jdk and cannot be accessed directly. We get the unsafe instance through reflection,
     UnsafeUtils This is a tool class. getUnsafe gets an unsafe instance. allocateInstance can create an instance of this type according to the type,
     And this instance is also a new instance, and it will not take the construction method
    */
    Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
    System.out.println("Unsafe Create instance:" + o);
}

@org.junit.Test
public void test4() throws InstantiationException {
    // The purpose of calling this static method is to trigger the loading initialization of Singleton1 class, which will cause the singleton object Singleton1 to be created
    Singleton1.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton1.getInstance());
    System.out.println(Singleton1.getInstance());

    // Deserialization destruction singleton
    unsafe(Singleton1.class);
}

It is found that Unsafe will create a new object, and Unsafe does not take the construction method

At present, there is no method to prevent single case of Unsafe destruction

2. Enumeration hungry Chinese style

Enumeration class is a syntax added by jdk5. It is very convenient to control the number of objects of enumeration class with enumeration class. Enumeration class will eventually be compiled into class by the compiler. It is still a class in essence, such as

enum Sex {
    MALE, FEMALE
}

After being compiled, it becomes binary bytecode. We translate it into java code as follows,

final class Sex extends Enum<Sex> {
    public static final Sex MALE;
    public static final Sex FEMALE;

    private Sex(String name, int ordinal) {
        super(name, ordinal);
    }

    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = values();
    }

    private static final Sex[] $VALUES;

    private static Sex[] $values() {
        return new Sex[]{MALE, FEMALE};
    }

    public static Sex[] values() {
        return $VALUES.clone();
    }

    public static Sex valueOf(String value) {
        return Enum.valueOf(Sex.class, value);
    }
}

Final means that it cannot be inherited. It corresponds to two static final member variables, and the type is Sex, and both are public. Private construction means that you cannot create a new object for this enumeration class through the new keyword. The name and sequence number of enumeration variables are incremented one by one from 0 and passed to the parent class construction. The static code block is mal, and FEMALE is initialized. They have created a Sex object, called their own private construction, and created two unique instances to mal, FEMALE these two variables. In the future, these two variables are the two Sex objects used

So we can use enumeration classes to create singleton patterns, that is, we can declare only one variable

package com.singleton.test;

// 2. Enumeration hungry Chinese style
public enum Singleton2 {
    // The control enumeration class has only one unique instance, and the enumeration variables are public
    INSTANCE;

    // The default enumeration class is private, which can be used without writing
    private Singleton2() {
        System.out.println("private Singleton2()");
    }

    // In order to print hashCode and see whether it is the same object, the name of enumeration class variable is not written by default
    @Override
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    // Provide static public methods to obtain static variables, because enumeration variables are public and can be used without providing them
    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    // To test whether it's hungry or lazy
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

Then start the test,

@org.junit.Test
public void test5(){
    // The purpose of calling this static method is to trigger the load initialization of Singleton2 class, which will cause the singleton object Singleton2 to be created
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());
}

It is found that the same object is used,

Benefit 1: prevent single instance of deserialization destruction

There are two advantages of using singleton for enumeration class. One is that it is not afraid to destroy singleton through deserialization and write tests,

private static void serializable(Object instance) throws IOException, ClassNotFoundException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(instance); // Turns the object into a byte stream
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
    // When the byte stream is restored to an object, deserialization will create a new object, and the deserialization does not go through the construction method
    System.out.println("Deserialize create instance:" + ois.readObject());
}

@org.junit.Test
public void test6() throws IOException, ClassNotFoundException {
    // The purpose of calling this static method is to trigger the load initialization of Singleton2 class, which will cause the singleton object Singleton2 to be created
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());

    // Deserialization destruction singleton
    serializable(Singleton2.class);
}

It is found that our Singleton2 enumeration does not write the readResolve method, and the same object is created by deserialization

Benefit 2: prevent single case of reflection damage

Second, it is not afraid to destroy single cases through reflection and write tests,

private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<?> constructor = clazz.getDeclaredConstructor(); // The parameter free construction method is obtained
        constructor.setAccessible(true); // Violent reflection, private construction methods can also be used
        System.out.println("Create instance of reflection:" + constructor.newInstance()); // Create instance
}

@org.junit.Test
public void test7() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    // The purpose of calling this static method is to trigger the load initialization of Singleton2 class, which will cause the singleton object Singleton2 to be created
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());

    // Single case of reflection destruction
    reflection(Singleton2.class);
}

At the same time, we have not rewritten the construction method in the Singleton2 enumeration

If an error is found, no parameter free Singleton2 construction method can be found,

We have also analyzed the construction method of enumeration. The construction of enumeration has two parameters, one is the name of enumeration variable and the other is sequence number. So we modify the code to get a parameterized construct and try to create a new enumeration object

private static void reflection1(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, Integer.class); // The parametric construction method is obtained
    constructor.setAccessible(true); // Violent reflection, private construction methods can also be used
    System.out.println("Create instance of reflection:" + constructor.newInstance("OTHER", 1)); // An attempt was made to create a new instance of an enumerated object
}

@org.junit.Test
public void test8() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    // The purpose of calling this static method is to trigger the load initialization of Singleton2 class, which will cause the singleton object Singleton2 to be created
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());

    // Single case of reflection destruction
    reflection1(Singleton2.class);
}

It still can't be found. You can't create an enumeration object through reflection

Single case of Unsafe failure

private static void unsafe(Class<?> clazz) throws InstantiationException {
    /*
     Unsafe It is a built-in class in jdk and cannot be accessed directly. We get the unsafe instance through reflection,
     UnsafeUtils This is a tool class. getUnsafe gets an unsafe instance. allocateInstance can create an instance of this type according to the type,
     And this instance is also a new instance, and it will not take the construction method
    */
    Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
    System.out.println("Unsafe Create instance:" + o);
}

@org.junit.Test
public void test9() throws InstantiationException {
    // The purpose of calling this static method is to trigger the load initialization of Singleton2 class, which will cause the singleton object Singleton2 to be created
    Singleton2.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton2.getInstance());
    System.out.println(Singleton2.getInstance());

    // Single case of Unsafe failure
    unsafe(Singleton2.class);
}

Found that a new object was created

At present, there is no method to prevent single case of Unsafe destruction

Extension: create a new enumeration object through Unsafe

package com.singleton.test;

import org.springframework.objenesis.instantiator.util.UnsafeUtils;
import sun.misc.Unsafe;

// Example: create an Enum object through Unsafe
public class EnumCreator {
    public static void main(String[] args) throws Exception {
        Unsafe unsafe = UnsafeUtils.getUnsafe();
        long nameOffset = unsafe.objectFieldOffset(Enum.class.getDeclaredField("name"));
        long ordinalOffset = unsafe.objectFieldOffset(Enum.class.getDeclaredField("ordinal"));
        Sex o = (Sex) unsafe.allocateInstance(Sex.class);
        unsafe.compareAndSwapObject(o, nameOffset, null, "Yin Yang people");
        unsafe.compareAndSwapInt(o, ordinalOffset, 0, 2);
        System.out.println(o.name());
        System.out.println(o.ordinal());
    }
}

3. Lazy style

package com.singleton.test;

import java.io.Serializable;

// 3. Lazy single example
public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    private static Singleton3 INSTANCE = null;

    // When the getInstance method is called, the object is not created before it is created
    public static Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

Write test code,

@org.junit.Test
    public void test10(){
    // The purpose of calling this static method: trigger the loading initialization of Singleton1 class and check whether the construct is called (distinguish between lazy and hungry)
    Singleton3.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton3.getInstance());
    System.out.println(Singleton3.getInstance());
}

It is found that when we first call the static method otherMethod, our construction method is not called. Only when the getInstance method is called and the object is not created, can we call to construct and create the object

Thread safety issues

At the same time, if our class runs in a multithreaded environment, thread safety must be considered. That is, if our multiple threads judge that the INSTANCE object is empty when they execute the getInstance method, multiple objects will be created.

We can directly add synchronized to getInstance for thread safety protection and synchronized static method, which is equivalent to adding a lock to this class (Singleton3.class)

When a thread executes this method, it attempts to get singleton3 If the lock of the class object is not held by other threads at this time, the thread will obtain the lock and execute the code in the method. At the same time, other threads need to wait. After the thread finishes executing, exit the method and release the lock, it can regain the lock and execute the code in the method again

However, the performance of this method is not high, because we only need to synchronize when the object is not created. If the object has been created, we don't need synchronization and mutual exclusion protection. Therefore, we hope to provide thread safety protection only when this object is created for the first time, and it is no longer needed after subsequent object creation. This leads to our fourth singleton creation method.

4.DCL (double check lock) lazy type

It is the optimization of our third singleton mode. Judge before locking. If an object is created, it will be returned directly. If no object is created, the competition between threads will be considered

package com.singleton.test;

import java.io.Serializable;

// 4. Lazy single case - DCL
public class Singleton4 implements Serializable {
    private Singleton4() {
        System.out.println("private Singleton4()");
    }

    private static volatile Singleton4 INSTANCE = null; // Visibility, order

    public static Singleton4 getInstance() {
        // Judge before locking. If an object is created, it will be returned directly. If no object is created, the competition between threads will be considered
        if (INSTANCE == null) {
            synchronized (Singleton4.class) {
                // Continue to judge that the object is created only when INSTANCE is null
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

At the same time, it should be noted that the member variable of our INSTANCE must be decorated with volatile

Why add the volatile modifier

Why add volatile for modification? We need to understand the line of install = new Singleton4 (). We decompile the binary bytecode file Singleton4.

First find the byte code file of Singleton4, and then right-click to select Open in Terminal

We input the decompile instruction, javap - C - V - P singleton4 Class, then find the getistance method and find the corresponding four instructions:

The new instruction means to create a new object (Singleton4). When creating an object, the member variables of the object will be calculated and the memory space required will be allocated when creating the object.

The invokespecial instruction refers to the calling method, the construction method, and the two-step operation when creating the object. The creation of the object is to allocate the memory space. After the object is created, there may be many member variables, and the assignment of these member variables is carried out in the construction method. Therefore, the first step is to allocate space and the second step is to initialize member variables.

The putstatic instruction means to assign a value to a static variable, that is, to our INSTANCE.

We know that the cpu may optimize (reorder) our instructions during execution to facilitate faster processing. If we don't make a causal relationship to the instructions, the cpu may change their execution order. The initialization of our construction method and the assignment of static variables are both initialization and assignment operations, so the cpu may change their order, which is no problem in a single thread, but there may be problems in multiple threads. As follows:

The cpu switches the construction method and the assignment of static variables. At this time, it only completes the initialization of the object and the assignment of static variables. The construction method has not been executed, that is, other member variables in the construction method have not been initialized, and other threads directly return the INSTANCE object, which is obviously wrong.

After adding volatile to modify INSTANCE, it will add a layer of memory barrier after the assignment statement of this variable when assigning a value to the shared variable INSTANCE. Its function is to prevent some assignment operations (write operations) before this statement from crossing it and running behind it. So the construction method can't go behind it.

Add: if you read the operation, you can't run in front of him (for details, please see the explanation of Volatile later)

Therefore, the following situations will not be a problem:

Case 1:

The thread holding has not completed the assignment operation for the static variable, so other threads can only wait for it to complete, so there will be no problem

Case 2:

What you get is a fully constructed object, so there will be no problem

Therefore, we need to add volatile to modify INSTANCE

Will there be thread safety problems with hungry Chinese style?

No, because the hungry Chinese style assigns the created object to the static member variable, and the static member variable will eventually be executed in the static code block of this class. The thread safety in the static code block is carried out by the java virtual machine to ensure the thread safety of object creation and code execution.

Therefore, if we put the creation of objects into static code blocks, it is thread safe. Therefore, this leads to the creation of our fifth singleton mode.

5. Internal lazy type

This method not only has the characteristics of lazy people, but also ensures thread safety when creating threads. It is recommended

package com.singleton.test;

import java.io.Serializable;

// 5. Lazy singleton - internal class
public class Singleton5 implements Serializable {
    private Singleton5() {
        System.out.println("private Singleton5()");
    }

    // Internal classes can access private variables and private constructs of external classes
    private static class Holder {
        // Assigning the created object to the static variable of the internal class will eventually be executed in the static code block, all of which are thread safe
        static Singleton5 INSTANCE = new Singleton5();
    }

    // Using the internal class to access its variables will trigger the load initialization of the internal class, that is, the creation of objects
    // It not only has the characteristics of lazy people, but also ensures thread safety when creating threads. It is recommended
    public static Singleton5 getInstance() {
        return Holder.INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

Write tests,

@org.junit.Test
public void test11(){
    // The purpose of calling this static method is to trigger the loading initialization of Singleton5 class and check whether the construct is called
    Singleton5.otherMethod();
    System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    System.out.println(Singleton5.getInstance());
    System.out.println(Singleton5.getInstance());
}

It is found that when we call the otherMethod static method, we do not trigger the creation of the singleton object, but only the load initialization of the external class. We do not load the internal class. When we call the getInstance static method, we will trigger the initialization of the internal class. At the same time, we can use the static code block to ensure the thread safety of creating the object during initialization.

2, Embodiment of singleton mode in JDK

Embodiment of singleton in JDK

  • The Runtime embodies the hungry man style singleton
  • Console embodies the double check lock lazy single example
  • The inner class lazy singleton of EmptyNavigableSet in Collections
  • ReverseComparator.REVERSE_ORDER internal lazy type single example
  • Comparators.NaturalOrderComparator.INSTANCE enumeration

Runtime class

System class

3, Volatile

Beep beep link: https://www.bilibili.com/video/BV15b4y117RJ?p=74

Previously, we have also initially understood the role of Volatile in DCL (double check lock) lazy style. Next, we will learn in detail.

Interview question: can volatile ensure thread safety? No, it can solve the visibility and ordering of shared variables, but it can't solve atomicity.

Visibility: when one thread modifies shared variables, another thread can see the latest results

Orderliness: the code in a thread is executed in the written order

Atomicity: multiple lines of code in a thread run as a whole, and no code of other threads can jump the queue during this period

Next, we will analyze them in turn. First, create a log object tool class LoggerUtils to output logs

package com.volatiles.test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

public class LoggerUtils {
    public static final Logger logger1 = LoggerFactory.getLogger("A");
    public static final Logger logger2 = LoggerFactory.getLogger("B");
    public static final Logger logger3 = LoggerFactory.getLogger("C");
    public static final Logger logger4 = LoggerFactory.getLogger("D");
    public static final Logger logger5 = LoggerFactory.getLogger("E");
    public static final Logger logger6 = LoggerFactory.getLogger("F");
    public static final Logger main = LoggerFactory.getLogger("G");

    private static final Map<String, Logger> map = new HashMap<>();

    static {
        map.put("1", logger1);
        map.put("2", logger2);
        map.put("3", logger3);
        map.put("4", logger4);
        map.put("5", logger5);
        map.put("6", logger6);
        map.put("0", logger6);
        map.put("main", main);
    }

    public static Logger get() {
        return get(null);
    }

    public static Logger get(String prefix) {
        String name = Thread.currentThread().getName();
        if(!name.equals("main")) {
            int length = name.length();
            name = name.substring(length - 1);
        }
        return map.getOrDefault(name, logger1);
    }

    public static void main(String[] args) {
        logger1.debug("hello");
        logger2.debug("hello");
        logger3.debug("hello");
        logger4.debug("hello");
        logger5.warn("hello");
        logger6.info("hello");
        main.error("hello");
    }
}

Atomic demonstration

The main idea of the following code is as follows: there is a shared variable balance with an initial value of 5. Use volatile to modify it, subtract it by 5 with the subtract method, and add it by 5 with the add method. First use two threads to execute the two methods respectively. After the two threads are running, finally view the result of balance

package com.volatiles.test;

import java.util.concurrent.CountDownLatch;

public class AddAndSubtract {

    static volatile int balance = 10;

    public static void subtract() {
        balance -= 5;
    }

    public static void add() {
        balance += 5;
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        new Thread(() -> {
            subtract();
            latch.countDown();
        }).start();
        new Thread(() -> {
            add();
            latch.countDown();
        }).start();
        latch.await();
        LoggerUtils.get().debug("{}", balance);
    }
}

We run the code and find that the result is 10, but this code has problems in our multithreading. There are two possibilities for the result, one is 5 and the other is 15. Because our balance -= 5 and balance += 5 are not atomic operations.

One line of commands on the surface may correspond to multiple lines of commands on the bottom. In the case of multithreading, if their execution order is not exchanged, there will be no problem. However, once they exchange execution sequences, there will be problems.

Continue to decompile this class (javap -c -v -p AddAndSubtract.class), and find that balance += 5 has changed to a 4-line command,

The getstatic instruction reads the value of a static variable, iconst_ The 5 instruction indicates that a number 5 is prepared, the iadd instruction indicates addition, and then the putstatic instruction writes the running result back to the static variable just read

We know that under multiple threads, cpu execution is switched back and forth under these threads, and the cpu may change their execution order (variables not modified by Volatile), resulting in incorrect execution results

We modify the code, change balance -= 5 and balance += 5 to multi line code, and mark the breakpoint for debugging

First, execute int b = balance of the subtract method. At this time, the value of B is 10, and then switch to the add method. After executing all the codes of the add method, the value of balance is 15, and then switch to the subtract method. At this time, the data with B of 10 is dirty data. Continue to execute, and the result is 5, which leads to the error of the result.

The final result is 5 and the correct result is 10, so Volatile can't solve the problem of atomicity

Visibility presentation

The main idea of the following code is as follows: we run a thread and change stop to true after 0.1s (the stop variable is not modified with volatile at this time). When stop is true, the while loop will be stopped and the number of runs will be output

package com.volatiles.test;


// Visibility example
public class ForeverLoop {
    static boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            LoggerUtils.get().debug("modify stop to true...");
        }).start();

        foo();
    }

    static void foo() {
        int i = 0;
        while (!stop) {
            i++;
        }
        LoggerUtils.get().debug("stopped... c:{}", i);
    }
}

When we run, we find that the java virtual machine does not stop running after 0.1s and does not output results. Obviously, we start a thread and set stop to true after 0.1s, but it has not stopped running. Why?

Did we not change the value of stop to true? We modify the code to write the status of multiple threads outputting stop after 0.2s

We found that the value of stop is true. We obviously modified the value of stop. Why hasn't it stopped running?

This is because the value of stop is initially false and stored in physical memory. Thread 1 executes the while loop code, and thread 2 modifies stop to true after 0.1s. We know that all the code is finally executed by the cpu.

First, the cpu reads the value of stop in the physical memory. The value read for the first time is false, so it continues to cycle to read the value of stop. This reading operation is very fast. It can read tens of millions of times in 0.1s. We know that the reading and writing efficiency of memory is relatively low, about hundreds of nanoseconds each time. When the cpu reads it so many times and finds that the value of stop is still false, the java real-time compiler JIT (which is a part of the java virtual machine and is mainly responsible for code optimization) will take effect.

Any of our java code will be translated into Java bytecode instructions, but the bytecode instructions can not be executed by the cpu. It also needs to pass through the interpreter, which will translate our Java bytecode instructions line by line into machine code, so that the cpu can recognize and execute them. However, the efficiency of translating Java bytecode instructions line by line into machine code is relatively low, so JIT will optimize some hot bytecode, that is, some frequently called and executed code.

Therefore, when our while loop exceeds the limit defined by JIT, it will be triggered by code optimization. Therefore, in order to reduce the operation between cpu and physical memory, he will automatically change our code, that is, change the value of stop to false. Originally, our bytecode was replaced by the compiled machine code by JIT and cached. After running this code, we will directly find the cached machine code and hand it over to the cpu for operation, thus reducing the process of intermediate interpretation and improving efficiency. Of course, if we need the previous code, it can also be replaced with the original code.

Therefore, even when other threads change the stop variable to true, thread 1 cannot stop running because its code has been replaced by JIT.

We can set the java virtual machine parameter, - Xint, which means that we only interpret and execute java bytecode, that is, we don't need JIT.

After re running, it is found that the number of cycles has been successfully output

However, we do not recommend this approach, because not using jit will affect the performance of the whole system. Therefore, we do not recommend this. We can solve the problem by directly modifying this variable with volatile, because once JIT finds that our variable is modified with volatile, it will not optimize this variable.

Orderly demonstration

A large number of tests must be carried out to expose the phenomenon of instruction reordering. Here we use jcstress core to test its maven coordinates

<dependency>
    <groupId>org.openjdk.jcstress</groupId>
    <artifactId>jcstress-core</artifactId>
    <version>0.14</version>
</dependency>

Write test code, x and y are not decorated with volatile, and run after packaging,

package com.volatiles.test;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;

// Ordering example
// Running instructions: Java - XX: + unlockdiagnosticvmoptions - XX: + logcompilation - jar jcstress jar -t day02. threadsafe. Reordering. Case1

public class Reordering {
    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    // The occurrence of 1,0 indicates that instruction reordering has occurred
    @Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @State
    public static class Case1 {
        int x;
        int y;

        // @Actor means that it will be executed by one thread during the test. The two here mean that two threads will be started to execute the two methods modified by @ actor
        // Operation of assignment
        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }
        // @Actor means that it will be executed by one thread during the test. The two here mean that two threads will be started to execute the two methods modified by @ actor
        // The operation of obtaining the value. This result will be compared with our expected value, II_Result indicates the collection result. The variables r.r1 and r.r2 will match the @ Outcome above
        // Compare the data in (multiple groups are separated by commas) to see whether the actual value is consistent with the expected value. Compare r.r1 with the first value in each group and r.r2 with the second value in each group,
        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }
}

The reordering of instructions was found,

Continue to write the second test, modify the y variable with volatile to prevent instruction reordering, and run after packaging,

package com.volatiles.test;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;

// Ordering example
// Running instructions: Java - XX: + unlockdiagnosticvmoptions - XX: + logcompilation - jar jcstress jar -t day02. threadsafe. Reordering. Case2

public class Reordering {
    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    // Expect. Forward indicates that if an instruction reordering occurs, an error will be reported directly
    @Outcome(id = "1, 0", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case2 {
        int x;
        // Modify the y variable with volatile to prevent instruction reordering
        volatile int y;

        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }

        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }
}

It is found that no error is reported, that is, there is no instruction reordering, and the throughput is reduced compared with that without volatile (M means ten million), because after using volatile, the variables modified by volatile cannot be optimized with JIT.

Continue to write the third test, modify the x variable with volatile to prevent the occurrence of instruction reordering. After packaging and running, it is found that an error is reported. Why?

Different effects of volatile location

Volatile uses memory barriers to solve instruction reordering. Different memory barriers will be added to write and read operations that add variables decorated with volatile. Write operations to volatile will add an upward barrier to prevent the above code from coming down, while read operations to volatile will add an up arrow and a down barrier, Prevent the following code from running above.

So if we use volatile to modify the x variable,

Write operation: the statement above x=1 cannot come down, but y=1 may go up,

Read operation: the statement after r.r2=x cannot run over the barrier, but r.r1=y can go down

If you like, please pay attention to me

So far, our single case mode and Volatile learning have come to an end. If you love my words, you can pay attention to my WeChat official account. I love learning, hee hee, and I share all kinds of resources from time to time.

Keywords: Java Multithreading Singleton pattern volatile

Added by tcl4p on Thu, 27 Jan 2022 01:09:46 +0200