Synchronized is a heavyweight lock, while volatile is a lightweight synchronized that guarantees the “visibility” of shared variables in multithreaded development. If a variable is volatile, it is less expensive than synchronized because it does not cause thread context switching and scheduling.

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. In plain English, this means that if a variable is volatile, Java ensures that all threads see the same value. If one thread makes an update to a volatile shared variable, other threads can see the update immediately. This is called thread visibility.

Memory model related concepts

Understanding volatile is a bit tricky because it has to do with Java’s memory model, so we need to understand the concept of the Java memory model before we can understand volatile.

Operating system semantics

When a computer runs a program, every instruction is executed in the CPU, and data reading and writing are bound to be involved in the execution process. We know that the data that the program is running is stored in main memory. One problem is that reading and writing data from main memory is not as fast as executing instructions from the CPU. If any interaction requires dealing with main memory, the efficiency is greatly affected. The CPU cache is unique to a CPU and is only relevant to the threads running on that CPU.

While CPU caching solves the problem of efficiency, it introduces a new problem: data consistency.

During the execution of the program, the data required for the execution is copied to the CPU cache. During the computation, the CPU no longer deals with main memory, but directly reads and writes data from the cache. Only after the execution is complete, the data is refreshed to main memory.

Here’s a simple example:

i = i + 1;
Copy the code

When the thread runs this code, it first reads the value of I from main memory (assuming I = 1 at this point), then copies it to the CPU cache, then the CPU performs the + 1 operation (I = 2 at this point), then writes the data I = 2 to the tell cache, and finally flusher it to main memory.

In fact, this is fine in a single thread, the problem is in multiple threads. As follows:

If two threads A and B both perform this operation (i++),Copy the code

Our normal logic would be that the value of I in main memory would be equal to 3.

But is that the case? Analysis is as follows:

Both threads read the value of I from main memory (assuming I = 1 at this point) into their respective caches, and then thread A performs the +1 operation and writes the result to the cache and finally to main memory, where I = 2. Thread B does the same thing, and I in main memory is still equal to 2. So you end up with 2, not 3. This phenomenon is the cache consistency problem.Copy the code

There are two solutions to cache consistency:

The cache consistency protocol is implemented by locking the busCopy the code

The problem with the first solution is that it is implemented in an exclusive way, that is, with the bus and LOCK# lock, only one CPU can run, and all the other cpus have to block, which is inefficient.

The second scheme, the Cache Consistency Protocol (MESI protocol), ensures that copies of shared variables used in each cache are consistent. The core idea is as follows: When a CPU writes data and finds that the variable is a shared variable, it notifies other cpus that the cache line of the variable is invalid. Therefore, when other cpus read the variable and find that it is invalid, they load data from main memory again.

Java memory model

Now let’s take a look at the Java memory model and take a look at what guarantees it provides and what methods and mechanisms it provides to ensure correct execution in multithreaded programming.

These three basic concepts are commonly encountered in concurrent programming: atomicity, visibility, and orderliness. Let’s look at volatile.

atomic

Atomicity: An operation or operations, either all performed without interruption by any factor, or none performed at all.

Atomicity is like a transaction in a database. Here is a simple example:

i = 0; // <1> j = i ; // <2> i++; // <3> i = j + 1; / / < 4 >Copy the code

Which of the above four operations are atomic and which are not? If you don’t understand it very well, you might think it’s all atomic, but in fact only one is atomic and nothing else is.

  1. In Java, variables and assignments to primitive data types are atomic operations.
  2. There are two operations: read I and assign the value of I to j.
  3. There are three operations: read I, I +1, and assign the +1 result to I.
  4. The same as < 3 >

Is reading and writing to 64-bit data atomic in the 64-bit JDK?

Read/write to plain longs and doubles should not be atomic (although it is OK to do so). Read/write to volatile longs and doubles must be atomic (optional)Copy the code

In addition, volatile does not guarantee atomicity for compound operations

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.

As analyzed above, in a multithreaded environment, one thread’s operations on a shared variable are invisible to other threads.

Java provides volatile to ensure visibility.

When a variable is volatile, thread-local memory is invalid.

When a thread modifies a shared variable, it is immediately updated to main memory.

When other threads read a shared variable, it reads directly from main memory.

Both Synchronize and locks ensure visibility.

order

Orderliness: that is, the order in which the program is executed is the order in which the code is executed.

In the Java memory model, the compiler and processor are allowed to reorder instructions for efficiency. Reordering does not affect single-thread execution, but it does affect multithreading.

Java provides volatile to ensure some order. The most famous example is the DCL (double-checked lock) in the singleton pattern.

Anatomy of the Volatile Principle

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

The above paragraph has two meanings:

Guaranteed visibility, not guaranteed atomicity disallows instruction reorderingCopy the code

I’m going to skip the first level of semantics and focus on instruction reordering.

Instruction reordering

To improve performance, compilers and processors often reorder instructions when executing a program:

Compiler reorder. The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program. Handler reorder. If there is no data dependency, the processor can change the execution order of the machine instructions corresponding to the statement.Copy the code

Instruction reordering has no effect on the single thread. It does not affect the running results of the program, but it will affect the correctness of multithreading. Since instruction reordering affects the correctness of multithreaded execution, we need to disable it. So how does the JVM prohibit reordering?

This leads to the happen-before principle

  1. Procedural order rule: in a thread, happens-before actions written earlier than actions written later, in code order.
  2. Lock rule: an unLock action, happens-before a lock action on the same lock.
  3. Volatile variable rule: Writes to a volatile variable are happens-before reads to that variable. Notice it’s in the back.
  4. The passing rule is: if A happens-before B and B happens-before C, then we can say that A happens-before C
  5. The Thread startup rule: the start method of the Thread object, happens-before each action of the Thread.
  6. Thread interrupt rule: A call to the threadinterrupt method, happens-before the interrupt thread’s code detects the occurrence of the interrupt event.
  7. Thread termination rule: All operations in a Thread are happens-before the Thread terminates. We can detect that the Thread has terminated by using thread.join () and the return value of thread.isalive ().
  8. Object finalization rule: an object’s initialization completes, happens-before the start of its Finalize () method

Let’s focus on the third Volatile rule: writes to Volatile variables, happen-before subsequent reads.

To implement volatile memory semantics, the JMM reorders as follows:

When the second operation is a volatile write, there is no reordering, regardless of the first operation. This rule ensures that operations that precede volatile writes are not reordered by the compiler after volatile writes.Copy the code

With a little knowledge of the happen-before principle, let’s answer this question how does the JVM prohibit reordering?

A look at the assembly code generated with and without the volatile keyword showed that the volatile keyword added a lock prefix. The lock prefix instruction acts as a memory barrier. A memory barrier is a set of processing instructions used to implement sequential restrictions on memory operations. The underlying layer of volatile is implemented through memory barriers.Copy the code

Here is the memory barrier required to complete the above rule:

conclusion

Volatile may seem simple, but understanding it can be tricky, and this is just a basic overview.

Volatile is lighter than synchronized, and can replace synchronized in some situations, but not entirely. Volatile should be used only in certain situations where two conditions must be met:

A write to a variable, independent of the current value. This variable is not contained in an invariant with other variables.Copy the code

Volatile is often used in the following scenarios: status-marker variables, Double Check. One thread writes multiple threads read.