CAS, escorting concurrent security at the hardware level

Keywords: security

1, Foreword

2, CAS operation

2.1 AtomInteger class ensures atomicity

Both synchronized and Lock are pessimistic locks. Here we learn a CAS operation, which is a lightweight Lock. There is an ABA problem in CAS operation. If the shared variables are one-way accumulation, this problem does not exist. If there are addition and subtraction, it needs to be handled through the Atomic class.

Volatile keyword is one of the 51 keywords used in Java. It is a keyword concurrent with multithreading, but it is generally not used in actual development. It is good to use the combination of synchronize+wait()+notify()/notifyAll() or lock+await()+signal()/signalAll(). But now we learn the knowledge of JVM, It is necessary for all to know volatile keyword and its underlying principle. Here is an example:

The main code is compiled into bytecode as follows:

Let's look at the Test.class file here and look at each script step by step, getstatic iconst_ Iadd putstatic analyzed the reason why the final result was less than 200000, which seemed to understand the problem fundamentally.

In fact, for Java programs, even if the. class file is printed, it is not rigorous to analyze bytecode one by one (because there is only one bytecode instruction compiled, it can not be said that this instruction is an atomic operation, that is, a bytecode is not necessarily an inseparable atomic operation). When interpreting and executing a bytecode instruction, The interpreter will have to run many lines of code to realize its semantics. If it is compiled and executed, a bytecode instruction may also be transformed into several local machine code instructions. It is worth noting here.

Question 1: why is it less than 200000?
Answer 1: because i + +; It is a three-step operation, not an atomic operation. To be safe in multithreading, it is necessary to ensure atomicity, visibility and order at the same time. Even if the volatile keyword is used, it is only a way to ensure visibility and order (the rational use of volatile prohibits instruction rearrangement). If atomicity is not guaranteed, the three-step operation can be interrupted, The processing method is as follows, which is completed using the atomic class AtomInteger.

Question 2: why can I do this using the atomic class AtomInteger?
Answer 2: synchronized == for + if(cas thread safety judgment). The for loop + if(cas judgment) package is used in the incrementAndGet() method to ensure thread safety, so it is finally equal to 20000. Therefore, the for + if(cas thread safety judgment) in the incrementAndGet() method ensures thread safety.

The AtomInteger class here is operated through CAS, which not only ensures atomicity, but also avoids the use of locks. The overall performance is much higher than that of Synchronized

2.2 CAS three-step operation

The compareAndSet() method of AtomicInteger class implements CAS operation through atomic operation, and the bottom layer is implemented based on assembly language.

Atom represents the smallest unit, so atomic operation can be regarded as the smallest execution unit. The operation will not be interrupted by any other task or event before execution.

CAS is an abbreviation of Compare And Set. The steps are as follows:
① Given the value current in the current memory and the expected value new to be modified, it is passed in;
② Compare the real value corresponding to the address of AtomicInteger object in memory (because it may not be modified) with real and current;
③ Equality means that the real has not been modified and is safe. Assign new to the real and then return. Inequality means that the real has been modified. End and re execute 1 until the modification is successful.

Thus, compared with Synchronized, CAS not only ensures atomicity, but also avoids the use of locks, and its overall performance is much higher than Synchronized

In the Java language, two locking strategies are provided to ensure thread safety

Optimistic Lock (Lock free operation): assuming that there is no conflict when all threads access shared resources, CAS is called comparison exchange to determine whether there is a conflict. If there is a conflict, retry the current operation until there is no conflict. In fact, the bottom layer of Lock is also CAS, which will be discussed later.

Pessimistic locks (built-in locks before JDK1.6): including synchronized and Lock. It is assumed that conflicts will occur every time the synchronized code block is executed. Therefore, when the thread obtains the Lock successfully, other threads trying to obtain the Lock will be blocked.

The two locking methods are compared as follows:

synchronizedCAS operation
Pessimistic lock. When there is thread competition, there will be performance problems caused by thread blocking and wake-up. Corresponding to mutually exclusive synchronization (blocking synchronization), the efficiency is very low.Optimistic lock does not directly suspend the thread. It will try CAS operations several times instead of time-consuming suspend and wake-up operations. Therefore, it is non blocking synchronization.

Question 1: the application of cas?
Answer 1: CAS has two applications, including lock to ensure that the atomic bottom layer uses CAS, and the incrementAndGet() method of AtomicInteger class ensures that the atomic bottom layer of i + + also uses CAS.

Question 2: why is lock more efficient than synchronized?
Answer 2: in terms of performance, if the competition for resources is not fierce, the performance of the two is similar. When the competition for resources is very fierce (that is, a large number of threads compete at the same time), the performance of Lock is much better than that of synchronized. Therefore, in the specific use, it should be selected according to the appropriate situation.

2.3 AtomicInteger class uses for loop + if(cas) to ensure atomicity

In the code of 2.1, the variable modified by volatile can only ensure the visibility and order in the case of multithreading. If it is necessary to ensure the atomicity of i + + operation, it is finally completed by using the incrementAndGet() method of AtomicInteger class, and the final value is equal to 20000, then this AtomicInteger class incrementAndGet() How does the method atomize the three-step operation of i + +? Let's take a look:

The AtomicInteger class incrementAndGet() method is as follows:

AtomicInteger class

public final int incrementAndGet() {
    for (;;) {
    	//Get current value
        int current = get();
        //Set expectations
        int next = current + 1;
        //Call the Native method compareAndSet to perform CAS operations
        if (compareAndSet(current, next))
        	//The expected value will be returned only after success, otherwise the wireless loop
            return next;
    }
}

compareAndSet is typically used as count, such as i + +, + i. here, i + + is taken as an example

As can be seen from the above, the incrementAndGet() method consists of "a for + an if". This for is the spin. Now, if it is not successful, it will cycle the spin. This if is to judge success. current == real, and directly return to next (it has been next=current+1, so it returns directly).

In the if conditional block of the incrementAndGet() method, the compareAndSet() method is called. This method accepts two parameters of current and next. This method is CAS operation, and the compareAndSet() method is implemented as follows:

AtomicInteger class

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

compareAndSet() method means: if the current status value is equal to the expected value, set the synchronization status to the given update value in an atomic manner.

Here, the argument valueOffset variable in the compareAndSwapInt() method is embedded and explained. First, the initialization of valueOffset is in the static code block, indicating the byte relative offset relative to the starting memory address:

private static final long valueOffset;
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
} 

After generating an AtomicInteger object, it can be regarded as generating a section of memory. The fields in the object are placed in this section of memory in a certain order, and the fields may not be placed continuously. The unsafe.objectFieldOffset(Field f) method accurately tells me the byte relative offset of the "value" field relative to the starting memory address of the AtomicInteger object.

private volatile int value;
  
public AtomicInteger(int initialValue) {
    value = initialValue;
}
  
public AtomicInteger() {
} 

In this program, value is a volatile variable. When different threads operate on this variable, it has visibility. Modification and write operations will be stored in main memory, and other CPUs will be notified that the variable cache line is invalid, ensuring that each read is the latest value.

Therefore, now we know that in the operation method incrementAndGet() of AtomicInteger class, spin is used instead of blocking (that is, for + if(CAS determines the current thread safety) instead of synchronized). Further, in the compareAndSet() method, CAS is used to realize this spin, that is, real==current must be returned to true to ensure security.

Let's go back to the compareAndSwapInt() method again. The method is defined in the sun.misc.Unsafe.java class. The method implementation is as follows:

/**
 * Atomically update Java variable to <tt>x</tt> if it is currently
 * holding <tt>expected</tt>.
 * @return <tt>true</tt> if successful
 */
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

Continue to find unsafe.cpp and find unsafe_ The entry() method is implemented as follows:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

At unsafe_ Inside the entry() method, the calling method is Atomic::cmpxchg. The source code of the method is as follows:

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
 
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

As shown in the above source code, Atomic::cmpxchg is implemented by embedded assembly. The CPU instruction is cmpxchg. The program will decide whether to add lock prefix to the cmpxchg instruction (i.e. LOCK_IF_MP(mp) code) according to the type of current processor.

For lock prefix instructions, i.e. for LOCK_IF_MP(mp) code, processed as follows:

① If the program is running on a multiprocessor, prefix the cmpxchg instruction with lock (lock cmpxchg);
② If the program is running on a single processor, omit the lock prefix (the single processor itself will maintain the order consistency within the single processor and does not need the memory barrier effect provided by the lock prefix)

For lock prefix instructions, i.e. for LOCK_IF_MP(mp) code, which is used to:

① Prohibit the reordering of the instruction with the previous and subsequent read and write instructions;
② Flushes all data in the write buffer into memory

In general, atomic (for example, AtomicInteger class) implements efficient lock free (the bottom layer still uses exclusive locks, that is, the lock prefix under multiprocessors, but the bottom layer processing is much faster than the java layer processing) and thread safety (volatile variable feature). While ensuring atomicity, it avoids the use of locks, and the overall performance is much higher than Synchronized

CAS is generally applicable to counting and multithreading programming. Multiple threads execute the methods under AtomicXXX class. When a thread executes, it is exclusive and will not be interrupted in the execution method. Other threads will not execute until the current thread is completed (CompareAndSet() is used for incrementAndGet() method in atomicinter class above) Method to ensure the thread safety of the add one operation, instead of synchronized synchronization blocking).

3, ABA problem of CAS

3.1 ABA problem definition

3.2 recurrence of ABA problems

public class ABADemo {
private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);

public static void main(String[] args) {
	new Thread(() -> {
		atomicReference.compareAndSet(100, 101);
		atomicReference.compareAndSet(101, 100);
	},"t1").start();
	
	new Thread(() -> {
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(atomicReference.compareAndSet(100, 2019) + "\t Modified value:" + atomicReference.get());
	},"t2").start();
}
}

Output results:

true Modified value:2019

Explanation of procedure:

CAS operation step 1: the initial value is 100
CAS operation step 2: thread t1 changes 100 to 101, and then changes 101 back to 100
CAS operation step 3: thread t2 sleeps for 1 second and waits for t1 operation to complete. Then thread t2 changes the value to 2019. You can see that thread 2 is successfully modified

3.3 ABA problem solving

Root cause: the reason for ABA problem is that traditional CAS only compares business values, current==real, which means that it has not been modified. Such comparison conditions are insufficient. For example, for 10 - > 11 - > 10, only the values are compared, and it is impossible to know whether the currently obtained value has been modified.

Knowing the cause of ABA problem, we know the breakthrough point to solve this problem: when obtaining the value, we should find a way to know whether the currently obtained value has been modified.

JDK processing method: two classes are provided to compare values and whether the current value has been modified, such as:

(1) AtomicStampedReference uses int to record the version number, indicating whether the current value has been modified (under the condition of current=real, if the version number is the same, it has not been modified, and if the version number is different, it has been modified);
(2) AtomicMarkableReference uses boolean to record whether the current value has been modified (under the condition of current=real, false means it has not been modified, and true means it has been modified).

Let's first look at the AtomicStampedReference class. Here we use the compareAndSet function of AtomicStampedReference, which has four parameters:

compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)

The first parameter expectedReference: indicates the expected value
The second parameter newReference: indicates the value to be updated
The third parameter, expectedStamp, indicates the expected timestamp
The fourth parameter newStamp: indicates the timestamp to be updated

How to implement the compareAndSet method? Let's go deep into the source code.

public boolean compareAndSet(V   expectedReference, V   newReference,
                             int expectedStamp, int newStamp) {
    Pair<V> current = pair;
    return expectedReference == current.reference && expectedStamp == current.stamp &&
        ((newReference == current.reference && newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

As you can see, the last returned value is compared with the value and version number at the same time

Pair class

private static class Pair<T> {
    final T reference;
    final int stamp;
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}
casPair()method

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

Connected together, as shown in the following figure:

Look at another AtomicMarkableReference class, which uses a boolean to record whether the current value has been modified. The source code is as follows:

AtomicMarkableReference class

public boolean compareAndSet(V expectedReference,V newReference,
                             boolean expectedMark,boolean newMark) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference && expectedMark == current.mark &&
        ((newReference == current.reference && newMark == current.mark) ||
         casPair(current, Pair.of(newReference, newMark)));
}
Pair class

private static class Pair<T> {
    final T reference;
    final boolean mark;
    private Pair(T reference, boolean mark) {
        this.reference = reference;
        this.mark = mark;
    }
    static <T> Pair<T> of(T reference, boolean mark) {
        return new Pair<T>(reference, mark);
    }
}
casPair()method

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

Connected together, as shown in the following figure:

3.4 code interpretation and ABA problem handling

3.4.1 solving ABA problems with atomicstampedreference class code

To solve the ABA problem, you can add a version number. When the value of memory location V is modified each time, the version number will be increased by 1.

AtomicStampedReference internally maintains the object value and version number. When creating an AtomicStampedReference object, you need to pass in the initial value and initial version number. When AtomicStampedReference sets the object value, the object value and status stamp must meet the expected value before writing can succeed.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {

    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("t1 Initial version number obtained:" + atomicStampedReference.getStamp());

            //Sleep for 1 second so that the t2 thread can get the same initial version number
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
        },"t1").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("t2 Initial version number obtained:" + stamp);

            //Sleep for 3 seconds to allow t1 thread to complete ABA operation
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Latest version number:" + atomicStampedReference.getStamp());
            System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t Current value:" + atomicStampedReference.getReference());
        },"t2").start();
    }
}

Output results:

t1 Initial version number obtained:1
t2 Initial version number obtained:1
 Latest version number:3
false Current value:100

Explanation of the program output:
Output 1: initial value 100, initial version number 1
Output 2: threads t1 and t2 get the same initial version number
Output 3: thread t1 completes ABA operation, and the version number is incremented to 3
Output 4: thread t2 completes the CAS operation. The latest version number has changed to 3, which is not equal to the version number 1 obtained by thread t2. The operation fails

Therefore, it can be seen that the return here is false, indicating that it has been modified, because it is true only when it is satisfied that not only the value is compared, but also whether the current value has been modified.

AtomicStampedReference can add the version number to the reference and track the whole change process of the reference, such as a - > b - > C - > D - > A. through AtomicStampedReference, we can know that the reference variable has been changed three times in the middle.

However, sometimes we don't care about how many times the reference variable has been changed, but just whether it has been changed. Therefore, we have the AtomicMarkableReference class. See below.

3.4.2 solving ABA problems with atomicmarkablereference code

The only difference of AtomicMarkableReference is that it no longer identifies the reference with int, but uses boolean variable to indicate whether the reference variable has been changed.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;

public class ABADemo2 {

    private static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<Integer>(100,false);

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("t1 Is the version number changed:" + atomicMarkableReference.isMarked());

            //Sleep for 1 second so that the t2 thread can get the same initial version number
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicMarkableReference.compareAndSet(100, 101,atomicMarkableReference.isMarked(),true);
            atomicMarkableReference.compareAndSet(101, 100,atomicMarkableReference.isMarked(),true);
        },"t1").start();

        new Thread(() -> {
            boolean isMarked = atomicMarkableReference.isMarked();
            System.out.println("t2 Is the version number changed:" + isMarked);

            //Sleep for 3 seconds to allow t1 thread to complete ABA operation
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Have you changed it:" + atomicMarkableReference.isMarked());
            System.out.println(atomicMarkableReference.compareAndSet(100, 2019,isMarked,true) + "\t Current value:" + atomicMarkableReference.getReference());
        },"t2").start();
    }
}

Output results:

t1 Is the version number changed:false
t2 Is the version number changed:false
 Have you changed it:true
false Current value:100

Explanation of the program output:
Output 1: the initial value is 100, and the initial version number has not been modified. false
Output 2: threads t1 and t2 get the same initial version number and have not been modified. false
Output 3: thread t1 completes ABA operation, and the version number is modified to true
Output 4: the CAS operation is completed by thread t2. The version number has become true, which is not equal to the version number false obtained by thread t2. The operation fails

Therefore, it can be seen that the return here is false, indicating that it has been modified, because it is true only when it is satisfied that not only the value is compared, but also whether the current value has been modified.

4, Interview golden finger

4.1 CAS operation

Question: what is CAS operation? How is it different from synchronized?
Answer: CAS is an abbreviation of Compare And Set. The steps are as follows:
① Given the value current in the current memory and the expected value new to be modified, it is passed in;
② Compare the real value corresponding to the address of AtomicInteger object in memory (because it may not be modified) with real and current;
③ Equality means that the real has not been modified and is safe. Assign new to the real and then return. Inequality means that the real has been modified. End and re execute 1 until the modification is successful.

Thus, compared with synchronized, CAS not only ensures atomicity, but also avoids the use of locks. The overall performance is much higher than synchronized. The differences between CAS and synchronized operations are as follows:

synchronized (also known as mutually exclusive synchronization / blocking synchronization)CAS (collision detection synchronization / non blocking synchronization)
lockPessimistic lock: 1. Lock first and then operate (lock first and then operate, and then release the lock after the operation is completed, so as to fair competition for all threads) 2. Pessimistic lock: hold a pessimistic attitude and fear of thread insecurity. All threads must be locked before each thread operation, which reduces the performance but improves the efficiency; 3. Blocking synchronization: threads that do not acquire locks, that is, threads that fail to compete for locks need to be suspended, wait() or wait (time parameter), and enter the blocked blocking state, so it is called blocking synchronization; 4. Implementation method: synchronized keyword and lock lock mechanismOptimistic lock: 1. Operate first. If there is no competition from other threads, the operation succeeds. If there is competition from other threads, conflicts will occur (conflicts are detected), and then take remedial measures; 2. Optimistic lock: hold an optimistic attitude, which can also be said to be a fluke mentality, and try again and again until success; 3. Non blocking synchronization: there is no blocking thread during the operation, so it is called non blocking synchronization. 4. Implementation mode: atomic operation of hardware. The following five are introduced, focusing on CAS
costThe operations of suspending and resuming threads need to be completed in the kernel state, which is costlyIt is not large. Using atomic operations directly does not need to block threads

4.2 the compareAndSet() method in atomicinteger class uses for+if(cas) to ensure thread safety

The biggest classic application of CAS is the compareAndSet() method in AtomicInteger class, which uses for+if(cas) to ensure thread safety.

Question: how can CAS operations ensure thread safety?
Answer: the compareAndSet() method in the AtomicInteger class uses for+if(cas) to ensure thread safety.
Explanation: CAS operations can be used in Java atomic classes, such as AtomicInteger class. For example, the compareAndSet() method in AtomicInteger class is implemented as follows:

return i++;How to atomize the three-step operation is completed in the previous three steps, just waiting for the right time to return.
public final int incrementAndGet() {
    for (;;) {
        int current = get();  // Step 1: read
        int next = current + 1;   // Step 2: calculate + 1,; Step 3: assign a value to next
        if (compareAndSet(current, next))   // All three steps have been completed, just waiting for the right time to return.
            return next;
    }
}

Question: how does this incrementAndGet() complete thread safe self increment operation in the case of multithreading without using synchronized blocking synchronization? That is, the hardware operation of i + + includes three steps: taking data from memory to register, i + + in the arithmetic unit, and putting data from the arithmetic unit into memory. Since synchronized is not used to block synchronization, how can the incrementAndGet() method in the AtomicInterger class ensure the atomicity of the operation in the case of multithreading?

Answer: synchronized = = for spin + if(CAS determines the current thread safety). Use for spin. In if, use CAS to determine the current thread safety. When the thread is safe, return to next (next=current+1, so return directly), so as to realize atomicity.

In synchronized = = for spin + if(CAS judges the current thread safety), we can know a lot by raising the if judgment (if is not raised in the source code because if is raised, the performance becomes worse when the package is large. Like synchronized, the performance becomes worse when the package is large):

public final int incrementAndGet() {
    for (;;) {
        if (compareAndSet(current, next)){
            int current = get();
            int next = current + 1;
            return next;
        }
    }
}

equivalence 

public final int incrementAndGet() {
    synchronized(this){
        int current = get();
        int next = current + 1;
        return next;
    }
}

Question: why can if be brought up to ensure atomicity?
Answer: to ensure atomicity is two points: to ensure that you enter the critical area after locking, and to ensure that you unlock the code after walking through the critical area.
(1) Thread safety is required before entering: cas and synchronized can be guaranteed. cas can enter only when current==real. Synchronized can enter only when it obtains a synchronization lock;
(2) The inside cannot go out and the outside cannot come in: cas and synchronized can guarantee that the cas return statement is return next;, It can be guaranteed that the data hasn't been changed before, so the inside doesn't go out and the outside can't come in; Synchronized can also ensure this. The lock is not released until the execution is completed, and the outside cannot enter.
Therefore, all cas packages can be rewritten as synchronized packages, and all synchronized packages can be rewritten as cas packages.

Note: cas does not participate in any actual business logic. cas only returns true|false as the basis for judging thread safety. It does not participate in the actual logic. It is the same as synchronized.

4.3 how does the bottom layer of CAS complete the comparison between current and real

The bottom layer is implemented with embedded assembly instructions. The CPU instruction is cmpxchg. The program will decide whether to add lock prefix (tip: LOCK_IF_MP(mp)) to the cmpxchg instruction according to the type of current processor.

For lock prefix instructions, i.e. for LOCK_IF_MP(mp) code, processed as follows:

① If the program is running on a multiprocessor, prefix the cmpxchg instruction with lock (lock cmpxchg);
② If the program is running on a single processor, omit the lock prefix (the single processor itself will maintain the order consistency within the single processor and does not need the memory barrier effect provided by the lock prefix)

For lock prefix instructions, i.e. for LOCK_IF_MP(mp) code, which is used to:

① Prohibit the reordering of the instruction with the previous and subsequent read and write instructions;
② Flushes all data in the write buffer into memory

4.4 spin, blocking, adaptive spin

Question 1: what is CAS?
Answer 1: cas is compare and swap. cas is to constantly compare N and O using compare. Once compare succeeds, change n, and then set swap and N to V. if not, return n and keep comparing.

Question 2: distinguish spin and blocking?
Answer 2: does the thread fail to stop, or does it keep trying to obtain the synchronization lock;
Blocking means that a thread stops after it fails to obtain a lock. It needs to wait for time or other threads to wake up (timing blocking + blocking).

Question 3: how to deal with the spin problem?
Answer 3: in order to solve the problem of spin, JDK adopts a processing mechanism: adaptive spin (dynamically adjust the spin time (number of cycle attempts) according to whether the lock can be obtained during spin waiting in the past).

Question 4: explain adaptive spin?
Answer 4: if the lock is obtained during the last spin and more opportunities are given, the spin time will be slightly longer this time;
If the lock has not been obtained at the end of the last spin, give less chance, and the spin time is a little shorter this time.

4.5 fairness of CAS operation

Question 1: fair model and unfair model?
Answer 1: (1) Fair mode: when a lock is waiting for many threads, the lock will select the thread with the longest waiting time to access its critical resources. It can be compared with the queue. It can be understood as the first come, first served principle (lock lock), which is fair.
(2) Unfair mode: when a lock can be preempted by subsequent threads, it is unfair, such as built-in locks (hunger problem: one or more threads starve to death because access permissions are always allocated to other threads).

Spin is also an unfair mode: threads in the blocked state cannot immediately compete for the released lock, and threads in the spin state are likely to obtain the lock first.

Question 2: talk about your understanding of fair lock? Is the spin operation of cas fair?
Answer 2: first, fair locks are meaningless. Enforcing fair locks will only reduce efficiency, and non fair locks can get better efficiency; This is why the synchronized weight lock is unfair, and the spin operation of cas is unfair. Lock is unfair by default, but fair lock can be realized. Therefore, from the jdk source code, fair lock is only an option, not a default recommendation.
Second, the meaning of fair lock: the bottom layer must use queue to implement the thread with the longest waiting time each time.
Third, the implementation of fair lock: synchronized weight lock is unfair, and the spin operation of cas is also unfair. Lock is unfair by default, but the wait queue can be used to realize fair lock.

5, Epilogue

CAS, escorting concurrent security at the hardware level, has been completed.

Make progress every day!!!

Posted by e11even on Wed, 03 Nov 2021 12:21:52 -0700