Hello volatile! Hello, synchronized!

This article has been published simultaneously on my personal homepage

Synchronized and Volatile are two keywords used to handle multi-threaded programming in Java. They are used to implement thread synchronization and Ensure visibility. In Java multi-threaded programming, the two are complementary, not antagonistic.

volatile

In Java, the volatile keyword can ensure the visibility of a variable. If we declare a variable as volatile, this indicates to the JVM that this variable is shared and unstable. Each time it is used it is read from main memory.

Command rearrangement

In Java, in addition to ensuring the visibility of variables, the volatile keyword also plays an important role in preventing JVM instruction reordering. If we declare a variable as volatile, when reading and writing this variable, a specific memory barrier will be inserted > to disable instruction reordering.

What is command rearrangement?

Simply put, it means that the code written in the program does not necessarily follow the order written when executed.

Java instruction rearrangement is a compiler and JVM (Java virtual machine) optimization technology. In order to make full use of the computing unit inside the processor, the processor may perform out-of-order execution optimization< on the input code. /strong>, the processor will reorganize the results of out-of-order execution after calculation and ensure that this result is consistent with the result of sequential execution, but this process does not guarantee the sequence of calculation of each statement. Same as the order in the Enter code. This is instruction reordering.

When will instruction reordering occur?

  1. Compiler rearrangement: The compiler may rearrange instructions when generating bytecode or native code to optimize the execution path. This rearrangement is usually performed without changing the semantics of the program.
  2. Processor rearrangement: Modern processors often have multi-stage pipelines that rearrange execution instructions to fully utilize hardware resources. Processor reordering occurs at the instruction level and does not change the semantics of the program but may affect visibility between threads.
  3. JVM rearrangement: The JVM may also rearrange Java bytecode instructions to improve performance. This rearrangement is also performed without changing the program semantics.

A recurring case of command rearrangement

Define four static variables x, y, a, b, and make them equal to 0 each time through the loop. Then use two threads, the first thread executes a=1; x=b; the second thread executes b=1;y=a.

Logically speaking, this programshould have three results:

  1. When the first thread executes to a=1, the second thread executes to b=1, and finally x=1, y=1;
  2. When the first thread finishes executing, the second thread just starts, and finallyx=0, y=1;
  3. When the second thread finishes executing, the first thread starts, and finallyx=1, y=0;

Theoretically, it is impossible for x=0, y=0 no matter what;

public class VolatileReOrderDemo {<!-- -->
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException{<!-- -->
        int i = 0;

        do {<!-- -->
            i + + ;
            x = 0; y = 0;
            a = 0; b = 0;

            //Open two threads, the first thread executes a=1;x=b; the second thread executes b=1;y=a
            Thread thread1 = new Thread(new Runnable() {<!-- -->
                @Override
                public void run() {<!-- -->
                    // Thread 1 will execute before thread 2, so use nanoTime to make thread 1 wait for thread 2 for 0.01 milliseconds.
                    shortWait(1000);

                    a = 1;
                    x = b;
                }
            });

            Thread thread2 = new Thread(new Runnable() {<!-- -->
                @Override
                public void run() {<!-- -->
                    b = 1;
                    y = a;
                }
            });

            thread1.start();
            thread2.start();

            thread1.join();
            thread2.join();

            // Splice the results after both threads have completed execution
            String result = String.format("%dth execution:\tx=%d\ty=%d", i, x, y);
            System.out.println(result);

        } while (x != 0 || y != 0);
    }

    /**
     * Uses a loop to wait for a period of time, but does not involve locks or notification mechanisms. It does not release the lock and does not wait for notification from other threads.
     * Only used for simple waiting for a period of time, not for cooperation between threads.
     *
     * @param interval waiting time interval
     */
    public static void shortWait(long interval){<!-- -->
        long start = System.nanoTime();
        long end;

        do {<!-- -->
            end = System.nanoTime();
        }while (start + interval >= end);
    }
}

In fact, when the program is run tens of thousands or millions of times, the result x=0,y=0; will appear:

The 4870627th execution: x=0 y=1
The 4870628th execution: x=0 y=1
The 4870629th execution: x=0 y=1
Execution 4870630: x=0 y=1
Execution 4870631: x=0 y=0

This is because the instructions are reordered, x=b is executed before a=1, and y=a is executed before b=1.

volatile prohibits command reordering

If we declare a variable as volatile, when reading and writing this variable, a specific memory barrier will be inserted to prevent instruction reordering.

Memory barrier is a CPU instruction that guarantees the execution order of specific operations.

The volatile keyword ensures that certain types of instruction reordering are prohibited. Around the read and write operations of volatile variables, the compiler and processor will add a memory barrier (Memory Barrier) to ensure that the write operation will not be reordered before the read operation, nor will it be reordered. Arranged after the read operation. This means that other threads reading the volatile variable will see the results of the write operation but not the reordering between them.

Singleton Pattern: A creational design pattern that ensures that there is only one instance of a class and provides a global access point to obtain that instance. The singleton pattern is typically used in situations where an instance needs to be shared globally across the application to ensure that the instance is only created once throughout the application life cycle.

/**
 * Double check lock implementation object singleton
 */
public class Singleton {<!-- -->
    private volatile static Singleton uniqueInstance;

    private Singleton(){<!-- -->

    }

    public static Singleton getUniqueInstance(){<!-- -->
        // Determine whether it has been instantiated. Enter the locking code if it has not been instantiated.
        if(uniqueInstance == null){<!-- -->
            // Class object lock
            synchronized (Singleton.class){<!-- -->
                if(uniqueInstance == null){<!-- -->
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

uniqueInstance = new Singleton(); This code is actually executed in three steps:

  1. Allocate memory space for uniqueInstance
  2. Initialize uniqueInstance
  3. Point uniqueInstance to the allocated memory address

However, due to the instruction rearrangement feature of the JVM, the execution order may become 1->3->2. Instruction reordering does not cause problems in a single-threaded environment, but in a multi-threaded environment it will cause a thread to obtain an instance that has not yet been initialized. For example, thread T1 executed 1 and 3. At this time, T2 called getUniqueInstance() and found that uniqueInstance was not empty, so it returned uniqueInstance, but At this time uniqueInstance has not yet been initialized.

volatile ensures the visibility of variables

Design the following program verification:

public class VolatileVisibilityDemo {<!-- -->
    private volatile boolean flag = false;

    public void toggleFlag(){<!-- -->
        //Modify the value of flag
        flag = !flag;
    }

    public boolean isFlag(){<!-- -->
        // read flag
        return flag;
    }

    public static void main(String[] args) {<!-- -->
        VolatileVisibilityDemo example = new VolatileVisibilityDemo();

        //Thread A: modify flag
        Thread threadA = new Thread(() -> {<!-- -->
            try {<!-- -->
                Thread.sleep(1000); // Give thread B enough time to start
            } catch (InterruptedException e) {<!-- -->
                e.printStackTrace();
                throw new RuntimeException(e);
            }
            example.toggleFlag();
            System.out.println("Flag has been set to true");
        });

        // Thread B: Check the value of flag
        Thread threadB = new Thread(() -> {<!-- -->
            while (!example.isFlag()){<!-- -->
                // Wait for flag to become true
            }
            System.out.println("Flag is now true");
        });

        threadA.start();
        threadB.start();
    }

}

In this example, there are two threads. Thread A is responsible for modifying the value of flag to true, while thread B is responsible for checking whether the value of flag is is true. Since flag is declared as volatile, thread B can immediately see the modification of flag by thread A, so thread B will be in flag changes to true and outputs the corresponding information.

If flag is declared as non-volatile, thread B may wait forever. Because visibility is not guaranteed, thread B cannot obtain thread A’s pair of flag in time. modification. This is the role of the volatile keyword, which ensures the visibility of variables and is suitable for situations where cross-thread communication is required.

volatile does not guarantee atomicity

The volatile keyword guarantees the visibility of variables, but it does not guarantee that operations on variables are atomic.

Atomicity is an important concept in multi-threaded programming. It means that an operation is indivisible, either all of it is executed or none of it is executed. If an operation is atomic, then in a multi-threaded environment, other threads cannot interfere with or modify its state during the execution of the operation, thus ensuring the consistency and integrity of the operation.

Verification example code:

/**
 * Verify that volatile does not guarantee the atomicity of variable operations
 */

public class VolatileAtomicityDemo {<!-- -->
    public volatile static int inc = 0;

    public void increase(){<!-- -->
        inc++;
    }

    public static void main(String[] args) throws InterruptedException{<!-- -->
        int numThreads = 5;
        int numIteration = 500;

        ExecutorService threadPool = Executors.newFixedThreadPool(numThreads);

        VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo();

        for (int i = 0; i < numThreads; i + + ){<!-- -->
            threadPool.execute(() -> {<!-- -->
                for (int j = 0; j < numIteration; j + + ){<!-- -->
                    volatileAtomicityDemo.increase();
                }
            });
        }

        // Wait 1.5 seconds to ensure the execution of the above program is completed
        Thread.sleep(15000);

        System.out.printf("There are %d threads in total, and each thread iterates %d times. It should have increased %d times!\\
",
                numThreads, numIteration, numThreads * numIteration);

        System.out.printf("The actual number of increases is: %d\\
", inc);

        threadPool.shutdown();
    }
}

The output is:

There are 5 threads in total, and each thread iterates 500 times. It should have grown 2500 times!
The actual number of increases is: 2417

Under normal circumstances, running the above code should output 2500. But after actually running the above code, you will find that many times the result is less than 2500.

In other words, if volatile can guarantee the atomicity of inc + + operation. After the inc variable is incremented in each thread, other threads can immediately see the modified value. Five threads performed 500 operations respectively, so the final value of inc should be 5*500=2500.

In fact, inc + + is a compound operation, including three steps:

  1. Read the value of inc.
  2. Add 1 to inc.
  3. Write the value of inc back to memory.

volatile cannot guarantee that these three operations are atomic, which may lead to the following situation:

  1. Thread 1 has not modified inc since it read it. Thread 2 reads the value of inc and modifies it (+ 1), and then writes the value of inc back to memory.
  2. After thread 2 completes the operation, thread 1 modifies the value of inc (+ 1), and then writes the value of inc back to the memory.

This also leads to the fact that after two threads perform an auto-increment operation on inc, inc actually only increases by 1.

In fact, if you want to ensure that the above code runs correctly, it is very simple to use synchronized, Lock or AtomicInteger.

synchronized

synchronized is a keyword in Java. The Chinese meaning is “synchronization” and “coordination”. It mainly solves the synchronicity of accessing resources between multiple threads. It can It is guaranteed that the method or code block modified by it can only be executed by one thread at any time.

Specific usage scenarios

Instance methods and static methods

In Java, methods can be divided into two major categories: instance methods and static methods. These two methods have different characteristics and uses.

  1. Instance Method:
    • Instance methods are methods associated with an object instance.
    • It can access and operate the instance variables (member variables) of the object.
    • Instance methods must be called through the object because they depend on the object’s state.
    • Typically used to encapsulate the behavior and operations of an object.

example:

public class Car {<!-- -->
    String model;

    public void start() {<!-- -->
        System.out.println("Starting the " + model + " car.");
    }
}

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        Car myCar = new Car();
        myCar.model = "Toyota";
        myCar.start(); // Call instance method
    }
}

In the above example, start is an instance method that operates the instance variable model of the Car class and must pass myCar instance call.

  1. Static Method:

    • Static methods are associated with classes, not object instances.

    • It cannot access the object’s instance variables because it is not tied to a specific instance.

    • Static methods can be called directly through the class name without creating an object.

    • Typically used to perform class-related operations that are not tied to a specific instance.

example:

public class MathUtils {<!-- -->
    public static int add(int a, int b) {<!-- -->
        return a + b;
    }
}

public class Main {<!-- -->
    public static void main(String[] args) {<!-- -->
        int result = MathUtils.add(5, 3); // Call static method
        System.out.println("Result: " + result);
    }
}

In the above example, add is a static method that is not associated with a specific object instance and can be called directly through the class name MathUtils.

Instance methods are associated with object instances and can access instance variables. Static methods are associated with class and do not depend on object instances and cannot access instance variables.

synchronized modified instance method

Lock the current object instance and obtain the lock of the current object instance before entering the synchronization code.

synchronized void method() {<!-- -->
    ...
}

synchronized modified static method

Locking the current class will affect all object instances of the class. Before entering the synchronization code, you must obtain the lock of the current class.

This is because static members do not belong to any instance object, they are owned by the entire class, they do not depend on a specific instance of the class, and they are shared by all instances of the class.

synchronized static void method() {<!-- -->
    ...
}

synchronized modified code block

Lock the specified object/class in brackets:

  • synchronized(object) means to obtain the lock of the given object before entering the synchronized code base.
  • synchronized(class.class) means to obtain the lock of the given Class before entering the synchronized code

The constructor cannot be modified with the synchronized keyword. Because the constructor itself is thread-safe, there is no such thing as a synchronous constructor.

synchronized ensures the visibility of operations

public class SynchronizedVisibilityDemo {<!-- -->
    private boolean flag = false;

    public synchronized void writeData(){<!-- -->
        flag = true;
    }

    public synchronized boolean readData(){<!-- -->
        return flag;
    }

    public static void main(String[] args) {<!-- -->
        SynchronizedVisibilityDemo demo = new SynchronizedVisibilityDemo();

        Thread writerThread = new Thread(() -> {<!-- -->
            // Ensure that the reading thread runs first
            try {<!-- -->
                Thread.sleep(1500);
            } catch (InterruptedException e) {<!-- -->
                throw new RuntimeException(e);
            }
            System.out.printf("Data is %s, ready to write to %s.\\
", demo.flag, !demo.flag);
            demo.writeData();
        });

        Thread readerThread = new Thread(() -> {<!-- -->
            while (true){<!-- -->
                // Wait for the data to become true
                if (demo.readData()) break;

            }
            System.out.println("Data is true now!");
        });


        readerThread.start();
        writerThread.start();
    }
}

In this example, if the synchronized keyword is not added, the writing thread’s operation on flag is invisible to the reading thread, and the reading thread will wait forever.

synchronized ensures the atomicity of operations

Similar to volatile‘s validation method. Without synchronized modification, the result may be:

There are 5 threads in total, and each thread iterates 1000 times. After growth Counter should be 5000!
Actual Counter: 4395

Test code:

public class SynchronizedAtomicityDemo {<!-- -->
    private int counter = 0;

    public synchronized void increase(){<!-- -->
        counter + + ;
    }

    public int getCounter(){<!-- -->
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {<!-- -->
        int numThreads = 5;
        int numIteration = 1000;

        ExecutorService threadPool = Executors.newFixedThreadPool(numThreads);

        SynchronizedAtomicityDemo demo = new SynchronizedAtomicityDemo();

        for (int i = 0; i < numThreads; i + + ) {<!-- -->
            threadPool.execute(() -> {<!-- -->
                for (int j = 0; j < numIteration; j + + ) {<!-- -->
                    demo.increase();
                }
            });
        }

        // Close the thread pool and wait for all tasks to complete
        threadPool.shutdown();

        boolean termination = threadPool.awaitTermination(1, TimeUnit.MINUTES);

        System.out.printf("There are %d threads in total, and each thread iterates %d times. After growth, Counter should be %d!\\
",
                numThreads, numIteration, numThreads * numIteration);

        System.out.println("Actual Counter: " + demo.getCounter());
    }
}

The underlying principle of synchronized

For modified code blocks

public class SynchronizedCodeBlockDemo {<!-- -->
    public void method(){<!-- -->
        synchronized (this){<!-- -->
            System.out.println("Synchronized code block");
        }
    }

    public static void main(String[] args) {<!-- -->
        SynchronizedCodeBlockDemo demo = new SynchronizedCodeBlockDemo();
        demo.method();
    }
}

Compile the code using javac, and then execute the javap -c -s -v -l SynchronizedCodeBlockDemo.class command, you can see the method method Decompilation result:

image-20231027134827905

Interpretation:

public void method();: This is the definition of the method, the name is method, there are no parameters, the return type is void, and it is declared as public.

descriptor: ()V: This part describes the descriptor of the method. () indicates that the method has no parameters, and V indicates that the return type is void.

flags: (0x0001) ACC_PUBLIC: This is the method modifier, indicating that this method is public.

Code:: The following are the bytecode instructions of the method.

stack=2, locals=3, args_size=1: This is the stack depth, number of local variables and number of parameters when the method is executed.

0: aload_0: Load the object reference to the operand stack.

1: dup: Copy the value on the top of the stack and push the copy onto the stack.

2: astore_1: Store the value at the top of the stack into local variable 1.

3: monitorenter: Enter the object monitor (lock object), used for the start of the synchronization block.

4: getstatic #7: Get a value from static field #7 (possibly a field in java/lang/System.out).

7: ldc #13: Load constant #13 (probably a string constant "Synchronized code block") onto the operand stack.

9: invokevirtual #15: Call virtual method #15 (probably java/io/PrintStream.println).

12: aload_1: Load local variable 1 onto the stack.

13: monitorexit: Exit the object monitor, used for the end of the synchronization block.

14: goto 22: Jump to the 22nd instruction.

17: astore_2: Store the exception object at the top of the stack into local variable 2.

18: aload_1: Load local variable 1 onto the stack.

19: monitorexit: Try to exit the object monitor again for exception handling.

20: aload_2: Load the exception object onto the stack.

21: throw: Throw an exception.

22: return: Return instruction, method execution ends.

Exception table:: This is the exception handling table, which describes which exceptions are handled in which scope.

LineNumberTable:: This is a line number table that specifies the correspondence between bytecode instructions and source code.

LocalVariableTable:: This is a local variable table that lists local variable information.

It is necessary to pay attention to the 3, 13 and 19 of the Code section, which indicates that the implementation of the synchronized synchronized statement block uses monitorenter and monitorexit directive, where the monitorenter directive points to the beginning of the synchronized code block, and the monitorexit directive specifies the end of the synchronized code block.

The above bytecode contains one monitorenter instruction and two monitorexit instructions. This is to ensure the normal execution of the code locked in the synchronized code block and the occurrence of exceptions. can be released correctly

For the case of modified methods

Operate the following code in the same way:

public class SynchronizedMethodDemo {<!-- -->
    public synchronized void method(){<!-- -->
        System.out.println("Synchronized method");
    }

    public static void main(String[] args) {<!-- -->
        SynchronizedMethodDemo demo = new SynchronizedMethodDemo();
        demo.method();
    }
}

image-20231027135643151

Compared with the modified code block, the synchronized modified method does not have the monitorenter instruction and the monitorexit instruction. Instead, it is indeed ACC_SYNCHRONIZED identifier, which indicates that the method is a synchronous method. The JVM uses the ACC_SYNCHRONIZED access flag to identify whether a method is declared as a synchronized method and perform corresponding synchronous calls.

If it is an instance method, the JVM will try to acquire the lock on the instance object. If it is a static method, the JVM will try to obtain the lock of the current class

flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED: This is the modifier of the method, indicating that this method is public and uses the synchronized modifier .

The essence of volatile and synchronized is to obtain the object monitor monitor.

The difference between synchronized and volatile

  • The volatile keyword is a lightweight implementation of thread synchronization, so the performance of volatile is definitely better than the synchronized keyword. However, the volatile keyword can only be used for variables and the synchronized keyword can be used to modify methods and code blocks.
  • The volatile keyword can guarantee the visibility of data, but it cannot guarantee the atomicity of data. The synchronized keyword guarantees both.
  • The volatile keyword is mainly used to solve the visibility of variables between multiple threads, while the synchronized keyword solves the synchronization of access to resources between multiple threads.

What is obtained instead is the ACC_SYNCHRONIZED flag, which indicates that the method is a synchronous method. The JVM uses the ACC_SYNCHRONIZED access flag to identify whether a method is declared as a synchronized method and perform corresponding synchronous calls.

If it is an instance method, the JVM will try to acquire the lock on the instance object. If it is a static method, the JVM will try to obtain the lock of the current class

flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED: This is the modifier of the method, indicating that this method is public and uses the synchronized modifier .

The essence of volatile and synchronized is to obtain the object monitor monitor.

The difference between synchronized and volatile

  • The volatile keyword is a lightweight implementation of thread synchronization, so the performance of volatile is definitely better than the synchronized keyword. However, the volatile keyword can only be used for variables and the synchronized keyword can be used to modify methods and code blocks.
  • The volatile keyword can guarantee the visibility of data, but it cannot guarantee the atomicity of data. The synchronized keyword guarantees both.
  • The volatile keyword is mainly used to solve the visibility of variables between multiple threads, while the synchronized keyword solves the synchronization of access to resources between multiple threads.
syntaxbug.com © 2021 All Rights Reserved.