1. Basic Concepts

1.1 Memory Model

In the process of program execution, it involves two aspects: instruction execution and data reading and writing. The execution of instructions is completed by the processor, while the reading and writing of data depends on the system memory, but the execution speed of the processor is much faster than the reading and writing of the memory data, so the cache is added in the processor. During the execution of the program, the data is first copied to the processor’s cache, and then written back to the system memory after the operation.

In the case of a single thread, this is not a problem, but in the case of multiple threads, exceptions can occur. For example, in the following code, I is a shared variable placed in the heap memory:

i = i + 1; // the initial value of I is 0.
Copy the code

If thread A and thread B both execute this code, one of two things can happen:

  • Case one: threadsATo perform first+ 1Operation, and then theiIs written back to system memory; threadBCopy from system memoryiThe value of the1Go to the cache and finish+ 1The operation is written back into system memory, and the final result isi=2.
  • Second case: threadsAAnd threadBFirst of all, all williThe value of the0Copy to the cache of the respective processor, threadAExecuted first+ 1Operation, and theniThe value of1, and then write back to system memory; But for threadsBIt does not know this process is in the cache of the processor running the threadiIs still equal to0, so before it executes+ 1After operation, then williIs written back into system memory, and the final result isi=1.

This uncertainty is called cache inconsistency.

1.2 Three concepts in concurrent programming

In concurrent programming, there are three key concepts: visibility, atomicity and orderliness. Only these three concepts can ensure the program to achieve the desired results in multithreading.

1.2.1 visibility

Visibility: This refers to the visibility between threads. The modified state of one thread is visible to another thread. That is, changes made by one thread are immediately visible to another thread. The example in 1.1 has visibility issues.

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

1.2.2 atomic

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

Or 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 us to use synchronization techniques to make it an atomic operation. If an operation is atomic, it is said to be atomic.

Synchronized and operate in Lock, unlock or atomic classes in Java to ensure atomicity.

1.2.3 order

Orderliness: that is, the order in which the program is executed is the order in which the code is executed. Take the following code for example:

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

The above code defines an integer and a Boolean variable and assigns values to them through statement 1 and statement 2, but the JVM does not guarantee that statement 1 will be executed before statement 2, meaning that instruction reordering may occur.

Instruction reordering refers to changing the order of statement execution to optimize the input code and improve the efficiency of the program on the premise that the final execution result of the program is consistent with the sequence execution result of the code.

However, this premise can be problematic in the case of multiple threads, as shown in the following code:

/ / thread 1:
context = loadContext(); //语句1
inited = true; //语句2
 
/ / thread 2:
while(! inited) { sleep() } doSomethingWithConfig(context);Copy the code

For thread 1, there is no dependency between statement 1 and statement 2, so instruction reordering is possible. But for thread 2, statement 2 is executed before statement 1, which results in the context not being initialized when the doSomethingWithConfig function is entered.

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

Volatile: Volatile

2.1 define

The definition of volatile is as follows: The Java programming language allows threads to access a shared variable, and to ensure that the shared variable is updated accurately and consistently, threads should ensure that the variable is acquired separately through an exclusive lock. If a field is declared volatile, the Java thread memory model ensures that all threads see the variable’s value as consistent.

Once a shared variable is volatile, there are two levels of semantics:

  • This ensures visibility when different threads operate on the variable, i.e. when one thread changes the value of a variable, the new value is immediately visible to other threads.
  • Command reordering is disabled.

Below, we explain these two layers of semantics with two summaries.

2.2 Ensure visibility

When we use a tool on an X86 processor to get the JIT compiler generated assembly instructions to see how to write to volatile, the following happens:

/ / Java code
instance = new Singleton(); // Instance is volatile

// Convert to assembly code
0x01a3de1d: move $0 x 0.0 x 1104800 (%esi); 
0x01a3de24: lock add1 $ 0 x 0, (%esp);
Copy the code

Writing to a volatile shared variable adds two lines of assembly code, and the lock-prefixed instruction causes two things on a multicore processor:

  • Writes data from the current processor’s internal cache back to system memory.
  • This write-back invalidates the data cached by other processors, and when those processors modify the data, they read it back from system memory into the processor cache.

2.3 Disabling command reordering

The volatile keyword disallows instruction reordering in two ways:

  • When the program executes tovolatileWhen a variable is read or written, all previous changes must have been made and the result is visible to subsequent operations; The operation behind it has certainly not taken place;
  • Instruction optimization cannot be performed in pairsvolatileVariable access statements are executed after it and cannot bevolatileThe statement following the variable is executed in front of it.

Take the following example:

//flag is volatile

x = 2; //语句1
y = 0; //语句2
flag = true;  //语句3
x = 4; //语句4
y = -1; 5 / / statement
Copy the code

Because flag is volatile, statement 1/2 is guaranteed to be executed before statement 3 and statement 4/5 after it, but the order between statements 1/2 or 4/5 is not guaranteed.

For Context problems mentioned in 1.2.3, we can ensure that the order between loadContext() and inited assignments is not changed by declaring the inited variable volatile. Avoid inited=true but the Context is not initialized.

2.4 Performance Problems

Volatile has two main advantages over synchronized: simplicity and performance. If from the reading and writing convenience to consider:

  • volatileRead operation overhead is very low, almost zerovolatileRead the same
  • volatileWrite operations are more expensive than nonvolatileThere are a lot more writes, because visibility needs to be implementedMemory is definedEven so,volatileThe total overhead is still lower than lock acquisition.volatileOperation will not be like a lockA blockage.

These two 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. Most programming situations run afoul of one of these two conditions, making volatile less commonly used for thread-safety than synchronized.

Thus, volatile can provide some scalability advantages over locking when it is safe to use them. If reads far outnumber writes, volatile variables can often reduce the performance cost of synchronization compared to locking.

2.5 Application Scenarios

For volatile variables to provide ideal thread-safety, both conditions must be met:

  • To the variableWrite operations do not depend on the current value. For example,x++Such an incremental operation, which is actually a sequence of read, modify, and write operations, must be performed atomically, andvolatileCannot provide the required atomic properties.
  • This variable is not contained in the invariants of other variables.

The most important rule against volatile abuse is to use volatile only when the state is truly independent of the rest of the program. Below, we summarize some of the situations in which volatile can be used.

2.5.1 Status flags

Use volatile to modify a Boolean status flag indicating that an important event, such as initialization completion or shutdown request, has occurred.

volatile booleanshutdownRequested; .public void shutdown(a) { shutdownRequested = true; }
 
public void doWork(a) { 
    while(! shutdownRequested) {// do stuff}}Copy the code

2.5.2 One-off Security Release

Before explaining what one-time secure publishing means, let’s take a look at the well-known double-checked locking problem in singletons.

    // Use volatile modifier.
    private volatile static Singleton sInstance;

    public static Singleton getInstance(a) {
        if (sInstance == null) { / / (0)
            synchronized (Singleton.class) { / / (1)
                if (sInstance == null) {  / / (2)
                    sInstance = new Singleton(); / / (3)}}}return sInstance;
    }
Copy the code

If the sInstance variable is not volatile, the following scenario might occur:

  • The first step:Thread1Enter thegetInstance()Method, due tosInstanceIs empty,Thread1Enter thesynchronizedCode block.
  • The second step:Thread1Move on to(3)Before the constructor is executedsInstanceObject becomes non-empty and setssInstanceThe memory space pointed to.
  • Step 3:Thread2Execute, it’s in the entry(0)Check if the instance is empty becausesInstanceObject is not empty,Thread2willsInstanceThe reference returns,At this timesInstanceThe object is not initialized.
  • Step 4:Thread1By running theSingletonObject constructor and returns a reference to it to complete the initialization of the object.

Volatile prevents reordering of steps 2 and 4 by making the initialization object complete before setting the memory space to which sInstance points.

2.5.3 Volatile Bean Mode

The Volatile Bean pattern applies to frameworks that use JavaBeans as honor structures. In the Volatile bean pattern, Javabeans are used as containers for a set of independent properties with getters and/or setter methods.

The rationale for the Volatile bean pattern is that many frameworks provide containers for holders of volatile data, but the objects placed in these containers must be thread-safe.

In the Volatile Bean pattern, all data members of javabeans are of volatile type, and getter and setter methods must be plain enough to contain no logic other than to get or set the corresponding property. In addition, for the data members referenced by the object, the referenced object must be effectively immutable.

public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;
 
    public String getFirstName(a) { return firstName; }
    public String getLastName(a) { return lastName; }
    public int getAge(a) { return age; }
    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }
    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }
    public void setAge(int age) { 
        this.age = age; }}Copy the code

2.5.4 Low-cost Read/Write Lock Policy

If reads far outstrip writes, you can use a combination of internal locks and volatile variables to reduce the overhead of common code paths. Synchronized is used in the following code to ensure that delta operations are atomic and volatile to ensure visibility of the current result. If updates are infrequent, this approach achieves better performance because the read path’s overhead involves only volatile reads, which is generally superior to the overhead of an uncontested lock acquisition.

public class CheesyCounter {
    private volatile int value;
    public int getValue(a) { return value; }
    public synchronized int increment(a) {
        returnvalue++; }}Copy the code

Iii. References

(1) Java concurrent programming: Volatile keyword parsing (2) Java volatile keyword parsing (3) Correct use of volatile variables (4) Use of volatile