Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This paper introduces the concept and characteristics of volatile keyword in Java in detail, and then analyzes the implementation of volatile keyword from code, bytecode, JVM, bottom CPU4 levels, and finally introduces the use of volatile keyword!

The Java language specification defines volatile as follows: The Java programming language allows threads to access a shared variable. To ensure that the shared variable is updated accurately and consistently, threads should ensure that the variable is acquired separately through exclusive locking. Java also provides the volatile keyword, which in some cases is more convenient than locking.

The volatile keyword is arguably the lightest synchronization mechanism available in the Java virtual machine, but it is not a lock. Therefore, when using volatile, you need to understand its characteristics and principles to use it properly.

1 Overview of Volatile

Volatile is arguably the lightest synchronization mechanism available in the Java virtual machine. Volatile does not cause thread context switching and scheduling, and is lighter than locking, but it is not a lock. Therefore, when using volatile, you need to understand its characteristics and principles to use it properly.

1.1 Characteristics of Volatile

  1. Visibility: This ensures visibility when different threads operate on a variable, i.e. when one thread changes the value of a variable, the new value is immediately visible to other threads.
  2. order: Disables command reordering.
    1. When a program reads or writes to a volatile variable, all preceding operations must have been performed and the results visible to subsequent operations. The operations that follow must not have been performed;
    2. During instruction optimization, statements that access volatile variables cannot be executed after them or statements that access volatile variables cannot be executed before them.
  3. Atomicity: read/write to any single volatile variable is atomic, but compound operations such as i++ are not.

1.2 Memory semantics for volatile

From a memory semantic point of view, volatile write-read has the same memory effect as lock release-acquire.

Memory semantics for volatile writes: When a volatile variable is written, the JMM flusher the shared variable from the thread’s local memory to main memory.

Memory semantics for volatile reads: When a volatile variable is read, the JMM invalidates the thread’s local memory. The thread will next read the shared variable from main memory.

Conclusion:

  1. When thread A writes A volatile variable, thread A essentially sends A message to A thread that will read the volatile variable next.
  2. Thread B reads a volatile variable, essentially receiving a message from a previous thread that made changes to the shared variable before writing to the volatile variable.
  3. Thread A writes A volatile variable, and thread B reads the volatile variable. Essentially, thread A sends A message through main memory to thread B.

The underlying implementation of Volatile

2.1 Code Level

The implementation at the code level is simple. Using the volatile keyword directly on a variable indicates that the variable is volatile and thus volatile.

The following cases:

/** * The code for volatile */
public class VolatileTest {
    /** * volatile */
    private static volatile int j = 1;

    public static void main(String[] args) { j++; }}Copy the code

2.2 Bytecode Level

Once the code is compiled to bytecode, is there any special implementation for the volatile keyword?

The implementation of volatile at the bytecode level can be found by removing the volatile keyword and viewing the generated bytecode instruction set using jclasslib, and then adding the volatile keyword and viewing the generated bytecode instruction set using jclasslib.

In practice, the difference between volatile and non-volatile is small, as follows:

Do not add a volatile:

Add a volatile:

In fact, we can see that the only difference at the bytecode level is the value of the access_flags- field scope for the field table.

If the field scope is 0x000A without volatile, the field access modifier table consists of ACC_PRIVATE and ACC_STATIC field access identifiers.

If the field scope of volatile is 0x004A, the query field access modifier table consists of ACC_PRIVATE, ACC_STATIC, and ACC_VOLATILE. Thus, we can assume that at the level of the bytecode instruction set, volatile is simply the addition of an ACC_VOLATILE field access identifier.

If you’re unfamiliar with bytecode and jclasslib tools, check out these two articles: Java’s Class file structure and Java’s JVM bytecode instruction set.

2.3 the JVM level

At the JVM level, the JVM level is implemented as Memory barriers according to jSR-133 (the Java Memory model and threading specification).

2.3.1 Overview of memory barriers

What is the memory barrier?

Memory barriers are CPU instructions. Due to CPU instructions may have reorder out-of-order execution, use memory barrier instruction, the instruction before and after the operation of the memory, the memory of the CPU, speaking, reading and writing to produce a sequence of constraints, to ensure that some specific operation execution order, guarantee the instruction execution order within a certain scope, organization of CPU instructions out-of-order optimization (heavy).

For the Java language, memory barriers are divided into system-level (CPU) memory barriers and JVM memory barriers.

2.3.1.1 Underlying System (CPU)

At the CPU level, different systems provide different instruction implementations. Common memory barrier instructions in x86/64 architecture include acquire, Release, fence, and Lock

Sfence: The write before the sfence directive must be completed before the write after the sfence directive.

Lfence: The read before the lfence directive must be completed before the read after the lfence directive.

Mfence: Read and write operations before the mfence directive must be completed before read and write operations after the mfence directive.

The lock: The instruction is a prefix instruction, In general, lock can be used with certain instructions (ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, And XCHG, etc.), which has the following features [summary from Intel manual] :

  1. Ensure that read – change – write operations on memory are performed atomically, that is, ensure that the instructions following lock become an atomic operation. In Pentium and processors prior to Pentium, instructions prefixed with lock lock the bus during execution, temporarily preventing other processors from accessing shared memory through the bus and ensuring exclusive memory. Obviously, this is expensive. Starting from Pentium4, Intel Xeon, and P6 processors, Intel uses Cache Locking to ensure atomicity of instruction execution. Cache locking greatly reduces the execution overhead of lock prefix instructions. This is actually CPU instruction level synchronization.
  2. Is not a memory barrier, but functions as a memory barrier to prevent reordering of this instruction with previous and subsequent read and write instructions.
  3. If the instruction following lock has a write operation, then lock causes all data in the write buffer to be flushed to main memory, invalidating other CPU-related cache lines and reloading the latest data from main memory.

2.3.2 JVM level

In order to achieve cross-platform performance, the Java memory model hides the differences between the underlying hardware platforms. The JVM generates the corresponding machine code for each platform. The JVM therefore defines its own “memory barrier.”

Java memory barriers are of Load and Store types:

  1. For Load barriers, inserting read barriers before reading instructions invalidates data in the cache and reloads data from main memory
  2. For a Store Barrier, inserting a write Barrier after a write instruction allows the most recent data written to the cache to be written back to main memory

The Java compiler inserts a memory barrier instruction in place when generating an instruction sequence to prevent reordering of certain types of instructions before and after the barrier. The Java memory model uses a conservative barrier insertion strategy, with volatile writes inserting barriers at the front and at the back, and volatile reads inserting two barriers at the back.

Specific requirements for memory barriers provided at the JVM level are as follows:

  1. Insert a StoreStore barrier (Store1, StoreStore, Store2) before each volatile write. In addition to ensuring that writes before and after a barrier cannot be reordered, this barrier also ensures that any read or write prior to volatile writes will be committed prior to volatile. Ensure the order in which write operations flush the cache.
  2. Insert a StoreLoad barrier after each volatile write, (Store1; StoreLoad; Load2). In addition to preventing volatile writes from reordering with subsequent reads, this barrier flushers the processor cache so that write updates to volatile variables are visible to other threads. The StoreLoad barrier is used in almost all modern multiprocessors, and in fact volatile writes are the storeLoad barrier used. The StoreLoad barrier prevents a subsequent load instruction from incorrectly using Store1 data, but it does not prevent another processor from writing new garbage data to the same memory location.
  3. Insert a LoadLoad barrier (Load1, LoadLoad,Load2) after each volatile read. In addition to preventing volatile reads from reordering from previous writes, the barrier flushers the processor cache so that volatile variables are read with the latest values.
  4. Insert a LoadStore barrier after each volatile read, (Load1; LoadStore; Store2). In addition to disallowing the reordering of volatile reads with any subsequent writes, the barrier flushers the processor cache so that write updates of volatile variables from other threads are visible to the thread with the volatile reads.

In addition, the JMM provides a volatile reordering table for compilers:

The first operation Second operation: normal read and write The second operation: volatile read The second operation: volatile write
General speaking, reading and writing You can rearrange You can rearrange It cannot be rearranged
Volatile read It cannot be rearranged It cannot be rearranged It cannot be rearranged
Volatile write You can rearrange It cannot be rearranged It cannot be rearranged

From the table we can see that:

  1. When the second operation is a volatile write, there is no reordering, regardless of the first operation. This rule ensures that operations before volatile writes are not reordered by the compiler after volatile writes.
  2. When the first operation is a volatile read, no matter what the second operation is, it cannot be reordered. This rule ensures that operations after volatile reads are not reordered by the compiler to those before volatile reads.
  3. When the first operation is volatile write and the second is volatile read, reorder cannot be performed.

X86 processors only reorder write-read operations. X86 does not reorder read-read, read-write, and write-write operations, so the memory barrier for these three types of operations is omitted in the X86 processor. In x86, the JMM can correctly implement the memory semantics of volatile write-read simply by inserting a StoreLoad barrier after volatile writes. This means that volatile writes are much more expensive than volatile reads on x86 processors (because executing the StoreLoad barrier is expensive).

2.4 Underlying System Layers

We’ve seen that the “memory barrier” is actually implemented at the JVM level, and we’ve also introduced the instruction level implementation at the bottom of the system. We know that the IMPLEMENTATION of the JVM actually depends on the implementation of the system’s underlying assembly language, so how does the process from JVM bytecode to the system’s underlying instructions work? Which instruction does the underlying system use, sfence? Lfence? Mfence or Lock? To support JVM mandated memory barriers?

As we’ve seen at the bytecode level, the JVM doesn’t generate any special bytecode for volatile fields, just an extra access modifier. In our case, the read and write of volatile and normal fields are essentially the _putstatic and _getstatic bytecode instructions. Is there anything special being done to the volatile modifier in the specific implementation of these two bytecode instructions? Go and have a look to know!

Here we need to look at the openjdk Hospot implementation of C++ source code. For the underlying implementation of the volatile system, the bytecodeinterpreter.cpp file in the Hospot source code, also known as the “C++ interpreter,” is used to parse the JVM bytecode instruction set.

We can find implementations of the Hospot interpreter for the _putstatic and _getstatic bytecode instructions.

2.4.1 Write volatile implementation

The _putstatic/_putfield bytecode directive is used to write static or instance properties

In the implementation of _putstatic, we can find the determination that the field is volatile and do additional processing:

if (cache->is_volatile()) {
  if (tos_type == itos) {
    obj->release_int_field_put(field_offset, STACK_INT(- 1));
  } else if (tos_type == atos) {
    VERIFY_OOP(STACK_OBJECT(- 1));
    obj->release_obj_field_put(field_offset, STACK_OBJECT(- 1));
    OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
  } else if (tos_type == btos) {
    obj->release_byte_field_put(field_offset, STACK_INT(- 1));
  } else if (tos_type == ltos) {
    obj->release_long_field_put(field_offset, STACK_LONG(- 1));
  } else if (tos_type == ctos) {
    obj->release_char_field_put(field_offset, STACK_INT(- 1));
  } else if (tos_type == stos) {
    obj->release_short_field_put(field_offset, STACK_INT(- 1));
  } else if (tos_type == ftos) {
    obj->release_float_field_put(field_offset, STACK_FLOAT(- 1));
  } else {
    obj->release_double_field_put(field_offset, STACK_DOUBLE(- 1));
  }
  OrderAccess::storeload(a);Copy the code

Cache ->is_volatile(), which is true if I is volatile, and then assigns the value of I. Release_xxx (type)_field_put is used.

Using int as an example, let’s look at the implementation of the release_int_field_put method, which is in oop.inline-hpp:

inline void oopDesc::release_int_field_put(int offset, jint contents) {
 OrderAccess::release_store(int_field_addr(offset), contents);  }
Copy the code

We see that the release_store method is called internally, which has different implementations in different system environments. Let’s look at the implementation in linux_X86, which is in orderAccess_linux_x86.inline-hpp () :

inline void     OrderAccess::release_store(volatile jint*    p, jint    v)
 { *p = v; }

Copy the code

We can see that the first parameter has the keyword volatile. Volatile is the C++ keyword.

Volatile is a type modifier that indicates that a variable declared by volatile is subject to change and is reloaded from memory each time a variable is read. And the compiler no longer optimizes code that operates on this variable, such as using out-of-order optimizations. In fact, C++ ‘s volatile also acts as a memory barrier.

After the assignment, the OrderAccess:: storeLoad () method is called. This is actually an implementation of the JVM’s memory barrier – storeload barrier, which is also found in orderAccess_linux_x86.inline-hpp. Together we also found implementations of three other barriers defined by JDK8:

inline void OrderAccess::loadload(a)   { acquire(a); }inline void OrderAccess::storestore(a) { release(a); }inline void OrderAccess::loadstore(a)  { acquire(a); }inline void OrderAccess::storeload(a)  { fence(a); }Copy the code

The storeload barrier (store, storeload, load) is implemented using the fence() method, as we have seen in our case, volatile (_putstatic) is inserted after assigning a volatile variable to Java:

// Implementation of the storeLoad barrier
inline void OrderAccess::fence(a) {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; Addl $0, 0 (RSP) % %" : : : "cc"."memory");
#else
    __asm__ volatile ("lock; Addl $0, 0 (% % esp)" : : : "cc"."memory");
#endif}}Copy the code

In this method, is_MP() is first used to determine if it is a multi-core CPU. If it is not, then it is ok because single-threaded CPU is fine. If it is a multi-core CPU, then it will execute the contents, either AMD64 or something else, usually other implementations, i.e. the storeLoad barrier is implemented by the following instructions:

__asm__ volatile ("lock; Addl $0, 0 (% % esp)" : : : "cc"."memory");
Copy the code

This is actually called inline assembly syntax and looks like this:

__asm__ (Instruction List: Out : In : Clobber);
Copy the code

Asm: is a macro definition of the GCC keyword ASM, which instructs the compiler to insert (inline) assembly statements here;

Instruction List: represents an assembly template that contains assembly Instruction lines.

Out: specifies the output of the current inline assembly statement.

In: specifies the input to the current inline assembly statement;

All three can be empty!

Clobber: Indicates a broken description that notifies GCC that some registers or memory may be modified by the current inline assembly statement and requires GCC to act accordingly to ensure consistency. Generally, the input and output are not specified

Let’s look at the assembly statement for the StoreLoad barrier:

_ volatile _ : _ volatile _ is a macro definition of the GCC keyword volatile (not the C++ volatile keyword, but essentially the same), which disallows the compiler to optimize code by treating the assembly as it is, with the assembly instructions in parentheses.

Memory: Destroys the descriptor to tell GCC that memory has been modified, and GCC guarantees that if the contents of a memory are loaded into a register before the inline assembly, then if the contents of that memory are needed after the inline assembly; Before this instruction, the necessary instructions are inserted to write the variable values in the register back to main memory, and when the instruction is read later, it will be read directly into this memory instead of using the copy stored in the register.

_ ASM _ VOLATILE _ (” “: : : “memory”) : Creates a compiler layer memory barrier and uses the instructions in parentheses as a memory barrier to tell the compiler not to optimize the access order of memory by crossing it, i.e., prohibiting reordering.

Addl $0,0(%% ESP) : indicates that the value 0 is added to the ESP register that points to the memory unit at the top of the stack. Add a 0, and the value of the ESP register remains the same. That is, this is a seemingly useless assembly instruction, but with the lock prefix instruction, you can synchronize the value of the write operation to main memory, in conjunction with the previous memory barrier, implementing the StoreLoad barrier. After the StoreLoad barrier is inserted, writes to Store1 are made visible to all processors before Load2 and all subsequent reads are executed.

Loadload (load1, loadload, load2) and loadStore (load, loadStore, store) are all implemented with acquire() :

// Implementation of loadLoad and loadStore barriers
inline void OrderAccess::acquire(a) {
  volatile intptr_t local_dummy;
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
  __asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}
Copy the code

After the barrier is inserted, the load before the barrier is completed before the load after the barrier can be executed to ensure that the data for the load is ready for the next store instruction.

The storeStore barrier (store1, storestore, store2) is implemented with release() :

// Implementation of the storeStore barrier
inline void OrderAccess::release(a) {
  // Avoid hitting the same cache-line from
  // different threads.
  volatile jint local_dummy = 0;
}
Copy the code

After the barrier is inserted, store operations before and after the barrier are performed, ensuring that data written to Store1 is visible to other cpus when store2 is executed.

2.4.2 Reading the Volatile implementation

_getStatic / _getField is used to read static or instance properties and puts the read results at the top of the stack.

In the implementation of _getstatic, we can find the determination that the field is volatile and do additional processing:

if (cache->is_volatile()) {
  if (tos_type == atos) {
    VERIFY_OOP(obj->obj_field_acquire(field_offset));
    SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), - 1);
  } else if (tos_type == itos) {
    SET_STACK_INT(obj->int_field_acquire(field_offset), - 1);
  } else if (tos_type == ltos) {
    SET_STACK_LONG(obj->long_field_acquire(field_offset), 0);
    MORE_STACK(1);
  } else if (tos_type == btos) {
    SET_STACK_INT(obj->byte_field_acquire(field_offset), - 1);
  } else if (tos_type == ctos) {
    SET_STACK_INT(obj->char_field_acquire(field_offset), - 1);
  } else if (tos_type == stos) {
    SET_STACK_INT(obj->short_field_acquire(field_offset), - 1);
  } else if (tos_type == ftos) {
    SET_STACK_FLOAT(obj->float_field_acquire(field_offset), - 1);
  } else {
    SET_STACK_DOUBLE(obj->double_field_acquire(field_offset), 0);
    MORE_STACK(1); }}Copy the code

Cache ->is_volatile(), which is true if I is volatile, and then obtains the value of I. This operation is implemented by XXX (type) _field_acquire.

Using int as an example, let’s look at the implementation of the int_field_acquire method, which is in oop.inline-hpp:

inline jint oopDesc::int_field_acquire(int offset) const      {
 return OrderAccess::load_acquire(int_field_addr(offset));      }
Copy the code

We can see that the acquire method is called internally, which has different implementations in different system environments. Let’s look at the implementation in Linux, which is in orderAccess_linux_x86.inline-hpp:

inline jint     OrderAccess::load_acquire(volatile jint*    p) { return 
*p; }
Copy the code

We can see that the first parameter has the keyword volatile. Volatile is the C++ keyword.

Volatile is a type modifier that indicates that a variable declared by volatile is subject to change and is reloaded from memory each time a variable is read. And the compiler no longer optimizes code that operates on this variable, such as out-of-order optimizations, which is where the memory barrier comes in.

Here we can see that reading volatile is controlled by using the C++ volatile keyword, without manually inserting the compiler barrier. We can also see that the C++ volatile keyword actually works the same way as the manually inserted compiler barrier [_ asm _ volatile _ (” “: : : “memory”)], preventing reordering and retrieving the latest values.

2.5 summarize

Volatile is implemented as follows:

  1. Code level: volatile keyword
  2. Bytecode level: ACC_VOLATILE field access identifier
  3. JVM level: THE JMM requires implementation as a memory barrier.
  4. (Hospot) System bottom layer:
    1. Read volatile the c++ based volatile keyword is read from main memory each time.
    2. Write volatile based on the c++ volatile keyword andlockThe memory barrier of the instruction, each time the new value is flushed to main memory, while other CPU cached values are invalidated.

C++ ‘s volatile prevents out-of-order optimization (reordering) of code associated with this variable and thus acts as a memory barrier, while the Linux kernel can also insert memory barriers manually: _ asm _ volatile _ (” “: : : “memory”).

2.5.1 JIT View assembly instruction

Let’s look at the effect of volatile on assembly instructions from a JIT perspective:

package com.thread.test.base.volatiles;

/** * The code for volatile */
public class VolatileTest {

    private  volatile int j;
    private   int s;
    private   int a;
    private   int b;

    public static void main(String[] args) {
        for (int i = 0; i < 1000000; i++) {
           newVolatileTest().SSS(); }}private  void SSS(a) {
        s = 0;
        s = 1;
        j = 1; a = s; b = j; }}Copy the code

Looking at the JIT-generated assembly code using HSDIS, you can find the following code:

3 The atomicity of the compound operation is not guaranteed

3.1 an overview of the

Because volatile is immediately visible to all threads, and writes to volatile immediately react to other threads, are operations on volatile variables safe in concurrency? This is wrong because volatile means that other threads immediately know that they are loading their own working memory with volatile data. If they make changes, the state of their volatile variables will be invalidated and re-read. However, if the thread’s variable has already been read into the stack frame, it will not be re-read; An overwrite problem occurs when both threads write the contents of the local working memory to main memory, leading to concurrency errors.

Although volatile requires that the (read, load, use), and (assign, Store, and write) values of variables be consecutively assigned as groups, the two sets of operations are kept separate. For example, if two threads complete the first set of operations (read, Load, use) at the same time, but do not complete the second set of operations (assign, Store, write), it is correct. Then the two threads start the second set of operations, and eventually one thread will overwrite the operation, resulting in inaccurate data.

3.2 case

public class VolatileTest1 {
    public static volatile int race = 0;
    public static final CountDownLatch countDownLatch = new CountDownLatch(10);

    static void add(a) {
        race++;
    }
    
    public static class VO implements Runnable {

        @Override
        public void run(a) {
            for (int j = 0; j < 1000; j++) { add(); } countDownLatch.countDown(); }}public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new VO());
            thread.start();
        }
        // Ensure that the first 10 threads are completedcountDownLatch.await(); System.out.println(race); }}Copy the code

The initial value race = 0 is 10 threads executing race++ at the same time, 1000 times each, and the final result may be less than 10000.

The reason is that each thread executes race++, which can be simply divided into the following 3 steps:

  1. The thread reads the value of the latest race from main memory to the execution engine;
  2. Incrementing race by 1 in the execution engine;
  3. The thread working memory flushes the race value to main memory;

It is possible that at some point two threads A and B both read 100 in Step 1 and get 101 after performing Step 2. At last, they refresh 101 twice and save it to main memory.

Because the rule for volatile variables in happens-before only specifies writes to a variable that are read to that variable after happens-before. So the intermediate process (Load to Store) is not secure.

For example, at step 2, thread B changes variable I, but thread A doesn’t know because thread A has already read the value of race. Only when thread A completes this loop and does the next read will the new value be reacquired due to visibility, but the values of thread A and thread B have already overwritten each other.

Let’s decompile race++ using javap to see the steps in more detail:

In the above instruction, “race++” is decomposed into four bytecode instructions:

  1. The getStatic command is used to bring the value of race to the top of the operand stack. Volatile can ensure that the value of RACE is correct at that point, but other threads may have increased the value of RACE by executing iconst_1, iADD, etc. Values at the top of the operand stack become stale data (note that the operand stack holds only values).
  2. Iconst_1 is used to push int constant 1 onto the operand stack.
  3. The iadd instruction simply adds the two int values at the top of the stack and pushes the result onto the stack, incrementing by 1.
  4. Putstatic assigns values to the static field of the class. The assigned values are the values computed by iADD, so it is possible for putStatic execution to synchronize smaller, older RACE values back into main memory.

When executing getStatic, both threads A and B must get the latest value of I, and then thread A increments and writes back to I. In this case, the main memory of I is the latest. However, B has already fetched I using getStatic and will not fetch it again. B then increments the value with the expired value, resulting in overwriting the value.

Summary and use of volatile

As you can see from the above case study, since volatile variables are only visible and only up-to-date, and a thread only needs to fetch a volatile variable once for each operation, what else is done with the volatile variable after it is retrieved, or whether the value is the latest value, Volatile is also not guaranteed, so atomicity still needs to be secured by locking in scenarios that do not comply with the following two rules:

  1. The result does not depend on the current value of the variable or ensures that only a single thread changes the value of the variable; Because relying on the current value would be a three-step operation of calculating and writing, these three steps are not atomic, and volatile does not guarantee atomicity.
  2. Variables do not need to participate in invariant constraints with other state variables. Basic operations, for example, are not atomic.

The application is summed up as “write once, read everywhere”, with one thread responsible for updating variables and the other thread only reading (not updating) variables and performing logic based on their new values. Such as:

  1. Status flags: Boolean status flags used to indicate that an important one-time event has occurred.
  2. Singleton mode: Solve the double-checked-locking problem.
  3. Observer mode flags a change in the value of a bit variable.

In addition, we can see that the implementation of volatile, synchronized, final, CAS operations in Java depends on the lock instruction. The basic principles of CAS can be found in this article: CAS implementation principles in Java parsing and Application.

References:

  1. Java Virtual Machine Specification
  2. The Beauty of Concurrent Programming in Java
  3. The Art of Concurrent Programming in Java
  4. Practical Java High Concurrency Programming

If you don’t understand or need to communicate, you can leave a message. In addition, I hope to like, collect, pay attention to, I will continue to update a variety of Java learning blog!