This is the 12th day of my participation in Gwen Challenge

In previous articles, we briefly introduced some of the basic concepts in Java threads, including synchronized locks. Synchronized locks are heavyweight locks, and while performance has improved over JDK iterations, they are still slightly more expensive than volatile. The volatile keyword does not cause thread context switching or scheduling.

JavaLanguage specificationvolatileKeyword definition

The Java programming language allows threads to access a shared variable, and to ensure that the shared variable can be updated accurately and consistently, threads should ensure that the variable is obtained separately through an exclusive lock.

“To ensure that shared variables are updated accurately and consistently,” here are some basic concepts of the Java memory model.

1. Basic concepts of Java memory model

Memory variable access

The Java memory model specifies that all variables are stored in main memory. When a computer runs a program, every instruction is completed in the CPU, and the data read and write in the running process will involve the interaction with the main memory data read and write. If every instruction interacts with the main memory, this efficiency will be very low, so there is the CPU cache. The CPU cache is unique to the CPU and is only relevant to the threads running on that CPU.

Each thread has its own working memory, which holds variables used by the thread (these variables are copied from main memory). All operations (reads, assignments) by a thread to a variable must be done in working memory. Different threads cannot directly access variables in each other’s working memory, and the transfer of variable values between threads needs to be completed through the main memory.

But this also introduces the problem of reading “dirty data” in multithreading. Here’s a simple example:

int a=1;
int i=1;
i += a;
Copy the code

If two threads perform I +=a in the above code, the result of the run may not be the expected 3. This is because there may be a case during the run:

At the beginning, the two threads read I =1 from main memory and cache it in their own memory. After the first thread completes the execution, the cache value of I in the second thread is still 1, and the final result of writing to main memory is 2.

The reason for this problem is to understand the three concepts of concurrent programming: atomicity, visibility, and orderliness.

1. The atomicity

That is, one or more operations are either all performed without interruption by any factor, or none at all.

The atom is the smallest unit in the world and indivisible. Such as a = 0; (a non-long and double) The operation is indivisible, so we say the operation is atomic. Another example: a++; This operation is actually a = a + 1; It’s divisible, so it’s not an atomic operation. All non-atomic operations have thread-safety issues, requiring sychronization to make it an atomic operation. If an operation is atomic, it is said to be atomic. Java provides a number of atomic classes under the Concurrent package, and you can read the API to see how these are used. For example: AtomicInteger, AtomicLong, AtomicReference, and so on.

Synchronized and lock and unlock ensure atomicity in Java. Volatile does not guarantee atomicity for compound operations

2. The visibility

Visibility means that when multiple threads access the same variable and one thread changes the value of the variable, other threads can immediately see the changed value.

Visibility refers to the visibility between threads, where the modified state of one thread is visible to another thread. That’s the result of a thread modification. Another thread will see that in a second. For example, variables that are volatile are visible. Volatile variables do not allow in-thread caching and reordering, that is, direct memory modification. So it’s visible to other threads. One caveat here, however, is that volatile only makes the content it modifiable visible, but it does not guarantee atomicity. For example, volatile int a = 0; And then we have an operation a++; The variable a is visible, but a++ is still a non-atomic operation, which also has thread-safety issues.

Implement visibility in Java for volatile, synchronized, and final.

3. The order

That is, the order in which the program is executed is the order in which the code is executed.

Here’s an example:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2
Copy the code

The above code defines an int variable, a Boolean variable, and then assigns to both variables. If statement 1 precedes statement 2 in code order, does the JVM guarantee that statement 1 precedes statement 2 when it actually executes this code? Not necessarily. Why? Instruction Reorder can occur here.

Instruction reordering. Generally speaking, the processor may optimize the input code to improve the efficiency of the program. It does not ensure that the execution sequence of each statement in the program is the same as that in the code, but it will ensure that the final execution result of the program is the same as that of the code execution sequence.

The Java language provides the keywords volatile and synchronized to ensure order between threads. Volatile because it contains the semantics of “forbid instruction reordering.” Synchronized is acquired by the rule that a variable can only be locked by one thread at a time, which determines that two synchronized blocks holding the same object lock can only be executed serially.

2. Volatile keyword resolution

Volatile guarantees thread visibility and provides some order, but not atomicity. Volatile is implemented using “memory barriers” underneath the JVM.

Once a shared variable (a member variable of a class, a static member variable of a class) is volatile, there are two levels of semantics:

  1. Make the variable visible to different threads, i.e. when one thread changes the value of a variable, the new value is immediately visible to other threads.
  2. Command reordering is disabled.

The Java language provides a weaker synchronization mechanism, known as volatile variables, to ensure that changes to variables are notified to other threads. When a variable is declared volatile, both the compiler and the runtime notice that the variable is shared and therefore do not reorder operations on it with other memory operations. Volatile variables are not cached in registers or hidden from other processors, so volatile variables always return the most recently written value when read.

Access to volatile variables does not lock and therefore does not block the thread, making them a lighter synchronization mechanism than the synchronized keyword.

When reading or writing non-volatile variables, each line first copies the variables from memory to the CPU cache. If the computer has multiple cpus, each thread may be processed on a different CPU, which means that each thread can be copied to a different CPU cache.

Declaring variables is volatile, and the JVM ensures that each read is read from memory, skipping the CPU cache.

2.1 Implementation mechanism of volatile

“Looking at the assembly code with and without volatile, we found that volatile had an extra lock prefix,” which acts as a memory barrier. The memory barrier provides three functions:

  1. It ensures that instruction reordering does not place subsequent instructions in front of the memory barrier, nor does it place previous instructions behind the barrier; That is, by the time the memory barrier instruction is executed, all operations in front of it have been completed;
  2. It forces changes to the cache to be written to main memory immediately.
  3. If it’s a write operation, it leads to something elseCPUThe corresponding cache row in.

3.2 volatile performance

Volatile has almost the same read cost as normal variables, but writes are slower because it requires inserting many memory-barrier instructions into the native code to keep the processor from executing out of order.

3. Scenarios where the volatile keyword is used

Synchronized prevents multiple threads from executing a piece of code at the same time, which can be very inefficient. Volatile is better than synchronized in some cases. Note, however, that volatile is no substitute for synchronized because volatile does not guarantee atomicity. In general, two conditions must be met to use volatile:

  • Writes to variables do not depend on the current value
  • This variable is not contained in an invariant with other variables

In effect, these conditions indicate that the valid values that can be written to volatile variables are independent of the state of any program, including the current state of the variables. In fact, my understanding is that the two conditions above require that the operation be atomic in order for a program that uses the volatile keyword to execute correctly on concurrency.

3.1 Scenario 1: Amount of status markers

volatile boolean flag = false;
 / / thread 1
while(! flag){ doSomething(); }/ / thread 2
public void setFlag(a) {
    flag = true;
}
Copy the code

Terminates the thread based on the status marker.

3.2 Scenario 2: Double Check in singleton mode

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton(a) {}public static Singleton getInstance(a) {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = newSingleton(); }}returninstance; }}Copy the code

4. To summarize

This article is just a summary of the simple use of volatile. See the article for more information:

  • The Volatile keyword in Java is explained
  • Java Concurrency — An in-depth look at how volatile works
  • Java concurrent programming: Volatile keyword resolution