introduction
In fact, when we explained the principle of synchronized keyword in Java Concurrent Programming in our last article, we talked about the concept of CAS many times, so what is it? In fact, in order to solve the thread safety problems caused by multi-threaded parallel execution, we often use the locking mechanism to change multi-threaded parallel execution into single threaded serial execution. In fact, there is another means to avoid such problems. This scheme is very different from the principle of mutual exclusion of synchronized keywords we analyzed before, In fact, it uses a spin lock free form to solve the thread safety problem. In the previous analysis, it is not difficult to find that when a thread wants to execute the synchronized modified code / method, in order to avoid conflicts during the operation of shared resources, it needs to execute the locking strategy every time. Without lock, it is always assumed that there is no conflict in the access to shared resources. The thread can execute continuously without locking and waiting, and everything is difficult, Once a conflict is found, is it really not handled in the lock free policy? Otherwise, the lock free strategy adopts a technology called CAS to ensure the security of thread execution. This CAS technology is the key to the implementation of lock free strategy.
1, Lockless landing executor - CAS mechanism
The lock free strategy we have been talking about sounds perfect, but how to implement it when it really needs to be used? The CAS mechanism is the implementation of our lock free policy. The full name of CAS is Compare And Swap. The implementation of CAS in Java ultimately depends on the atomic instruction implementation of CPU (we will analyze it later). The core idea of CAS mechanism is as follows:
CAS(V,E,N)
- 5: Shared variables requiring action
- E: Expected value
- N: New value
If the value of V is equal to the value of E, the value of N is assigned to v. Conversely, if the value of V is not equal to the value of E, it means that other threads have changed V before the current thread writes back, and the current thread does nothing. In short, when a thread needs to make changes to V, save the value of the shared variable at the current time before the operation. When the thread needs to write back the new value after the operation is completed, first re obtain the comparison between the latest variable value and the saved expected value before the operation. If the same indicates that no other thread has changed, the current thread will execute the write operation. However, if the expected value is inconsistent with that saved before the current thread operation, it indicates that the value has been modified by other threads. At this time, the update operation is not performed, but you can choose to re read the variable and try to modify the variable again, or you can give up the operation. The schematic diagram is as follows:
Because CAS operations are optimistic, each time a thread operates, it thinks it can execute successfully. When multiple threads use CAS to operate a variable at the same time, only one will execute successfully and update successfully, and the rest will fail. However, the failed thread will not be suspended. It is only informed of the failure and allowed to try again. Of course, the failed thread is also allowed to give up the operation. Based on this principle, CAS mechanism can also know that other threads have operated on shared resources and execute corresponding processing measures even if there is no lock. At the same time, because there is no lock in CAS lock free operation, it is impossible to have a deadlock. Therefore, we can also draw a conclusion: "CAS is naturally immune to deadlock", because CAS itself is not locked.
1.1. Operating system support for CAS mechanism
Then, some small partners will wonder if there will be NO security problems and inconsistencies when multiple threads perform CAS operations at the same time? Because the CAS operation is not completed in one step, but is also executed in multiple steps. Is it possible to switch the thread and change the value when you are about to assign a value after judging that V and E are the same? The answer is very certain: NO, Why? As I mentioned earlier, the final implementation of CAS mechanism in Java depends on the implementation of CPU atomic instructions. CAS is an instruction in the category of operating system primitives, which is composed of several instructions. It is used to complete a function, and the execution of primitives must be continuous. It is not allowed to be interrupted in the execution process, In other words, CAS is an atomic instruction for the CPU and will not cause the so-called data inconsistency problem.
2, Magic pointer class in Java - Unsafe
The Unsafe class is located in the sun.misc package. It means Unsafe in Chinese translation. When we first see this class, we may be surprised why there is a class named "Unsafe" in the JDK. However, it is not difficult to find the magic of this class in detail. It provides very powerful functions, but there is some insecurity. The Unsafe class exists in the sun.misc package. Its internal method operation can directly operate the memory like the pointer of C. when we can directly operate the memory like the pointer of C through this class, this kind of insecurity is highlighted, which means:
- Not managed by the JVM means that it cannot be GC. We need to release the memory manually. When you use this class to do some operations, a memory leak will occur
- Many methods in the Unsafe class must provide the original address (memory address) and the address of the replaced object. The offset must be calculated by yourself. Once a problem occurs, it is an error at the JVM crash level, which will lead to the collapse of the entire Java program, which is manifested as the direct crash of the application process
However, the direct operation of memory through unsafe class also means that its speed will be faster than that of ordinary Java programs, which can improve the efficiency under the condition of high concurrency. Therefore, from the above perspectives, although it improves the efficiency to a certain extent, it also brings the insecurity of pointers. Unsafe is worthy of its name. Therefore, if there are no special requirements when writing programs, we should not consider using it, and Java officials do not recommend it (unsafe does not provide external constructors), and Oracle officials originally planned to remove unsafe classes from Java 9. However, due to the extensive use of this class in the Java Concurrent package, Oracle did not remove the unsafe class in Java, but did relative optimization and maintenance. In addition to contracting, frameworks such as Netty also frequently use this class in the bottom layer. Therefore, we should be glad to retain this class, otherwise there will be fewer excellent open source frameworks. However, it is worth noting that all methods in unsafe class are modified native ly, that is, the methods in unsafe class directly call the underlying resources of the operating system to perform corresponding tasks. The main function points of unsafe class are as follows:
- Class correlation: provides class and its static domain manipulation methods
- Info related: returns some low-level memory information
- Arrays correlation: provides array manipulation methods
- Objects correlation: provides Object and its domain manipulation methods
- Memory related: provide direct memory access method (bypass JVM heap and directly manipulate local Memory)
- Synchronization related: provides manipulation methods such as low-level synchronization primitives and thread suspension / drop
2.1. Memory management: the method of directly manipulating memory provided by Unsafe class
//Allocates memory of the specified size public native long allocateMemory(long bytes); //Reallocate the specified size of memory according to the given memory address setting public native long reallocateMemory(long address, long bytes); //Memory used to free allocateMemory and reallocateMemory requests public native void freeMemory(long address); //Sets all bytes in the memory block of the given offset offset of the specified object to a fixed value public native void setMemory(Object o, long offset, long bytes, byte value); //Sets the value of the given memory address public native void putAddress(long address, long x); //Gets the value of the specified memory address public native long getAddress(long address); //Sets the long value for the given memory address public native void putLong(long address, long x); //Gets the long value of the specified memory address public native long getLong(long address); //Sets or gets the byte value of the specified memory public native byte getByte(long address); public native void putByte(long address, byte x); //The operations of other basic data types (long,char,float,double,short, etc.) are the same as putByte and getByte .......... Omit code //Memory page size of the operating system public native int pageSize();
2.2. Object instance creation: Unsafe class provides a new way to create object instances
In the past, when we created class object instances, we created them in two forms: new and reflection. However, whether they were created in the form of new or reflection, they would call the construction method of the object to complete the initialization of the object. The Unsafe class provides a new way to create object instances, as follows:
//Pass in the class of an object and create the instance object, but the constructor will not be called public native Object allocateInstance(Class cls) throws InstantiationException;
2.3. Class, instance object and variable operation: Unsafe class provides class, instance object and variable manipulation methods
//Gets the offset of the field f in the instance object public native long objectFieldOffset(Field f); //The offset of the static attribute, which is used to read and write the static attribute in the corresponding Class object public native long staticFieldOffset(Field f); //The return value is f.getdeclarangclass() public native Object staticFieldBase(Field f); //Obtain the int value on the offset of a given object. The so-called offset can be simply understood as the pointer pointing to the memory address of the variable, //The variable of the object can be obtained through the offset for various operations public native int getInt(Object o, long offset); //Sets the int value of the offset on the given object public native void putInt(Object o, long offset, int x); //Gets the value of the reference type at the given object offset public native Object getObject(Object o, long offset); //Sets the value of the reference type on the given object offset public native void putObject(Object o, long offset, Object x); //The operations of other basic data types (long,char,byte,float,double) are the same as getInthe and putInt //Set the int value of a given object and use volatile semantics, that is, it is immediately updated to memory and visible to other threads public native void putIntVolatile(Object o, long offset, int x); //Get the int value of the specified offset offset of a given object. Using volatile semantics, you can always get the latest int value. public native int getIntVolatile(Object o, long offset); //Operations of other basic data types (long,char,byte,float,double) are the same as putIntVolatile //It is the same as getIntVolatile, and the reference type putObjectVolatile is the same. ..........Omit code //It is the same as putIntVolatile, but the operated field must be modified with volatile public native void putOrderedInt(Object o,long offset,int x);
Let's use a small Demo to deepen your familiarity with Unsafe classes:
The Unsafe class does not provide an external constructor. Although the Unsafe class provides an external getUnsafe() method, this method is only provided to the Bootstrap class loader. Ordinary user calls will throw exceptions. Therefore, we use reflection technology to obtain the Unsafe instance object and perform relevant operations in the following Demo.
public static Unsafe getUnsafe() { Class cc = sun.reflect.Reflection.getCallerClass(2); if (cc.getClassLoader() != null) throw new SecurityException("Unsafe"); return theUnsafe; }
public class UnSafeDemo { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException { // Get the Field object corresponding to theUnsafe through reflection Field field = Unsafe.class.getDeclaredField("theUnsafe"); // Set the Field to be accessible field.setAccessible(true); // Get the specific object corresponding to the Field through the Field. null is passed in because the Field is static Unsafe unsafe = (Unsafe) field.get(null); System.out.println(unsafe); //Create objects directly through allocateInstance Demo demo = (Demo) unsafe.allocateInstance(Demo.class); Class demoClass = demo.getClass(); Field str = demoClass.getDeclaredField("str"); Field i = demoClass.getDeclaredField("i"); Field staticStr = demoClass.getDeclaredField("staticStr"); //Get the offset of the instance variables str and i in the object memory and set the value unsafe.putInt(demo,unsafe.objectFieldOffset(i),1); unsafe.putObject(demo,unsafe.objectFieldOffset(str),"Hello Word!"); // Return to User.class Object staticField = unsafe.staticFieldBase(staticStr); System.out.println("staticField:" + staticStr); //Gets the offset staticOffset of the static variable staticStr long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("staticStr")); //Gets the value of a static variable System.out.println("Before setting Static field value:"+unsafe.getObject(staticField,staticOffset)); //Set value unsafe.putObject(staticField,staticOffset,"Hello Java!"); //Get the value of the static variable again System.out.println("After setting Static field value:"+unsafe.getObject(staticField,staticOffset)); //Call toString method System.out.println("Output results:"+demo.toString()); long data = 1000; byte size = 1; //Unit byte //Call allocateMemory to allocate memory and obtain the memory address memoryAddress long memoryAddress = unsafe.allocateMemory(size); //Write data directly to memory unsafe.putAddress(memoryAddress, data); //Gets the data of the specified memory address long addrData = unsafe.getAddress(memoryAddress); System.out.println("addrData:"+addrData); /** * Output results: sun.misc.Unsafe@0f18aef2 staticField:class com.demo.Demo Static field value before setting: Demo_Static Static field value after setting: Hello Java! Output USER:Demo{str='Hello Word!', i='1', staticStr='Hello Java! '} addrData:1000 */ } } class Demo{ public Demo(){ System.out.println("I am Demo Class constructor, I was called to create an object instance...."); } private String str; private int i; private static String staticStr = "Demo_Static"; @Override public String toString() { return "Demo{" + "str = '" + str + '\'' + ", i = '" + i +'\'' + ", staticStr = " + staticStr +'\'' + '}'; } }
2.4. Array operation: Unsafe class provides a way to directly obtain the memory location of array elements
//Gets the offset address of the first element of the array public native int arrayBaseOffset(Class arrayClass); //The memory space occupied by an element in the array. arrayBaseOffset is used with arrayIndexScale to locate the position of each element in the array in memory public native int arrayIndexScale(Class arrayClass);
2.4 CAS related operations: Unsafe class provides CAS operation support in Java
//The first parameter o is the given object, and offset is the offset of the object memory. Through this offset, you can quickly locate the field and set or obtain the value of the field, //Expected indicates the expected value, and x indicates the value to be set. The following three methods all perform operations through CAS atomic instructions. public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x); public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
2.5 new method based on the original CAS method after JDK8
//1.8 add: for a given object o, add delta according to the field pointed to by the obtained memory offset, //This is a CAS operation process. You can exit the loop and return the old value until the setting is successful public final int getAndAddInt(Object o, long offset, int delta) { int v; do { //Get the latest value in memory v = getIntVolatile(o, offset); //Operation through CAS } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; } //1.8 new. The function of the method is the same as above, except for the long type data operated here public final long getAndAddLong(Object o, long offset, long delta) { long v; do { v = getLongVolatile(o, offset); } while (!compareAndSwapLong(o, offset, v, v + delta)); return v; } //1.8 add: given the object o, set the field to the new value newValue according to the obtained memory offset, //This is a CAS operation process. You can exit the loop and return the old value until the setting is successful public final int getAndSetInt(Object o, long offset, int newValue) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, newValue)); return v; } // 1.8 add, the same as above, and the operation is of long type public final long getAndSetLong(Object o, long offset, long newValue) { long v; do { v = getLongVolatile(o, offset); } while (!compareAndSwapLong(o, offset, v, newValue)); return v; } //1.8 new, the same as above, operates on reference type data public final Object getAndSetObject(Object o, long offset, Object newValue) { Object v; do { v = getObjectVolatile(o, offset); } while (!compareAndSwapObject(o, offset, v, newValue)); return v; }
In the above way, if a partner has seen the source code of the Atomic package of JDK8, they should also be able to see them.
2.6. Thread operation: Unsafe class provides thread suspension and recovery operation support
Suspending a thread is implemented through the park method. After calling Park, the thread will block until timeout or interrupt conditions occur. unpark can terminate a suspended thread and restore it to normal. The Java thread suspension operation is encapsulated in the LockSupport class. There are various versions of pack methods in the LockSupport class, and its underlying implementation is finally implemented by the Unsafe.park() method and Unsafe.unpark() method.
//When a thread calls this method, the thread will block until it times out or an interrupt condition occurs. public native void park(boolean isAbsolute, long time); //Terminate the suspended thread and return to normal. The suspended operations in the. java.util.concurrent package are implemented in the LockSupport class. The underlying layer uses these two methods, public native void unpark(Object thread);
2.7 memory barrier: mentioned in the previous article JMM, instruction reordering Memory barrier definition support for operations such as
//All read operations before this method must be completed before the load barrier public native void loadFence(); //All write operations before this method must be completed before the store barrier public native void storeFence(); //All read and write operations before this method must be completed before the full barrier. This memory barrier is equivalent to the combination of the above two functions public native void fullFence();
2.8. Other operations: please refer to for more operations JDK8 official API document
//Get hold lock, not recommended @Deprecated public native void monitorEnter(Object var1); //Release lock, not recommended @Deprecated public native void monitorExit(Object var1); //Attempt to acquire lock, deprecated @Deprecated public native boolean tryMonitorEnter(Object var1); //Gets the number of pages in native memory. This value is always to the power of 2 public native int pageSize(); //Tell the virtual machine to define a class without security check. By default, the class loader and protection domain come from the caller class public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); //Load an anonymous class public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches); //Determine whether a class needs to be loaded public native boolean shouldBeInitialized(Class<?> c); //Ensure that the class must be loaded public native void ensureClassInitialized(Class<?> c);
3, Atomic operation package under J.U.C package
Through the analysis in the previous part, we should basically understand the lock free idea of CAS and have a comprehensive understanding of the magic class Unsafe, which are also the basic conditions for us to analyze the atomic package. Then we will analyze the application of CAS in Java step by step. The JUC concurrent package launched after JDK5 provides java.util.concurrent.atomic atomic package, which provides a large number of atomic operation classes based on CAS. If you do not want to lock the program code but still want to avoid thread safety problems, you can use the classes provided under the package. The operation classes provided by the atomic package can be divided into the following four types:
- Basic type atomic operation class
- Reference type atomic operation class
- Array type atomic operation class
- Property update atomic operation class
3.1 basic type atomic operation class
The Atomic operation classes for basic types provided in the Atomic package are AtomicInteger, AtomicBoolean and AtomicLong respectively. Their underlying implementation and use methods are consistent. Therefore, we only analyze one AtomicInteger as an example. AtomicInteger mainly performs Atomic operations on int type data. It provides Atomic self increment method API s such as Atomic subtraction method and Atomic assignment method:
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // Get pointer class Unsafe class instance private static final Unsafe unsafe = Unsafe.getUnsafe(); //The memory offset of the variable value within the AtomicInteger instance object private static final long valueOffset; static { try { //Get the offset of the value variable in the object memory through the objectFieldOffset() method of the unsafe class //With this offset valueOffset, the internal method of the unsafe class can obtain the variable value and perform value or assignment operations on it valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } //int variable value encapsulated by the current AtomicInteger private volatile int value; public AtomicInteger(int initialValue) { value = initialValue; } public AtomicInteger() { } //Get the current latest value, public final int get() { return value; } //Setting the current value has the volatile effect. The method is decorated with final to further ensure thread safety public final void set(int newValue) { value = newValue; } //Finally, it will be set to newValue. After using this method, other threads may get the old value in a short period of time, which is similar to delayed loading public final void lazySet(int newValue) { unsafe.putOrderedInt(this, valueOffset, newValue); } //Set the new value and get the old value. The underlying call is the CAS operation, that is, the unsafe.compareAndSwapInt() method public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue); } //If the current value is expect, it is set to update (the current value refers to the value variable) public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } //The current value increases by 1, the old value is returned, and the underlying CAS operation public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } //The current value is deducted by 1, the old value is returned, and the underlying CAS operation public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1); } //Increase the current value by delta, return the old value, and perform the underlying CAS operation public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } //The current value increases by 1 and returns the new value after the increase. The underlying CAS operation public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } //Subtract 1 from the current value and return the new value after subtraction. The underlying CAS operation public final int decrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, -1) - 1; } //delta is added to the current value, a new value is returned, and the underlying CAS operation public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; } //Omit some unusual methods }
From the above source code, we can know that all methods of AtomicInteger atomic class do not use any mutex mechanism to achieve synchronization, but ensure thread safety through CAS operations provided by Unsafe class, which we described earlier Analysis of Synchronized principle The article mentioned that i + + actually has thread safety problems, so let's observe the self increment implementation incrementAndGet in the AtomicInteger atomic class:
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
From the above source code, we can see that the final implementation is completed by calling the unsafe.getAndAddInt() method. From our previous analysis, we know that this method is a new method based on the original cas operation after JDK8, and we further follow up:
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
It can be seen that getAndAddInt keeps trying to update the value to be set through a do while loop until it succeeds. It calls the compareAndSwapInt method in the Unsafe class, which is a CAS operation method. Note: in the source code implementation before Java 8, the for loop is implemented directly in the AtomicInteger class. In Java 8, it is replaced by while and moved to the Unsafe class.
Let's master the usage of atomic operation classes of basic types through a simple demo:
public class AtomicIntegerDemo { // Create shared variable atomicI static AtomicInteger atomicI = new AtomicInteger(); public static class AddThread implements Runnable{ public void run(){ for(int i = 0; i < 10000; i++) atomicI.incrementAndGet(); } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; //Start five threads and execute the autoincrement operation of atomicI at the same time for(int i = 1; i <= 5; i++){ threads[i]=new Thread(new AddThread()); } //Start thread for(int i = 1; i <= 5; i++){threads[i].start();} for(int i = 1; i <= 5; i++){threads[i].join();} System.out.println(atomicI); } } //Output result: 50000
In the above demo, AtomicInteger is used to replace the original int, so that thread safety can be guaranteed without locking. AtomicBoolean and AtomicLong will no longer be analyzed, and their use and principle are the same.
3.2 reference type atomic operation class
The atomic operation class of reference type mainly analyzes AtomicReference class. Other principles and uses are consistent. This paragraph will not be repeated in the subsequent content. Let's take a look at a demo first:
public class AtomicReferenceDemo { public static AtomicReference<Student> atomicStudentRef = new AtomicReference<Student>(); public static void main(String[] args) { Student s1 = new Student(1, "Bamboo"); atomicStudentRef.set(s1); Student s2 = new Student(2, "panda"); atomicStudentRef.compareAndSet(s1, s2); System.out.println(atomicStudentRef.get().toString()); //Execution result: Student{id=2, name = "Panda"} } static class Student { private int id; public String name; public Student(int id, String name) { this.id = id; this.name = name; } public String getName() { return name; } @Override public String toString() { return "Student{" +"id=" + id +", name=" + name +"}"; } } }
Before that, we analyzed that the underlying implementation of the atomic counter AtomicInteger class is implemented through CAS operations in the Unsafe class. How do we implement the underlying AtomicReference now?
public class AtomicReference<V> implements java.io.Serializable { // Get unsafe object instance private static final Unsafe unsafe = Unsafe.getUnsafe(); // Define value offset private static final long valueOffset; static { try { /* The static code block assigns a value to the offset when the class is loaded, and obtains the value of the class attribute through the provided by the unsafe class The address API gets the address of the attribute value defined by the current class */ valueOffset = unsafe.objectFieldOffset (AtomicReference.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } //Internal variable value, which can be obtained by valueOffset memory offset of Unsafe class private volatile V value; /* Atomic substitution method, indirectly calling compareAndSwapObject() of Unsafe class, It is a native method that implements CAS operations */ public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); } //Set and get old values public final V getAndSet(V newValue) { return (V)unsafe.getAndSetObject(this, valueOffset, newValue); } //Omit code } //The getAndSetObject method in Unsafe class is actually called by CAS operation public final Object getAndSetObject(Object o, long offset, Object newValue) { Object v; do { v = getObjectVolatile(o, offset); } while (!compareAndSwapObject(o, offset, v, newValue)); return v; }
From the above source code, we can see that AtomicReference is basically the same as the AtomicInteger implementation principle we analyzed earlier, and is finally implemented through CAS operations provided in the Unsafe class. The implementation principles of other methods of AtomicReference are also roughly the same, but it should be noted that Java 8 adds several API s to AtomicReference:
- getAndUpdate(UnaryOperator)
- updateAndGet(UnaryOperator)
- getAndAccumulate(V,AnaryOperator)
- accumulateAndGet(V,AnaryOperator)
The above methods exist in almost all atomic classes, and these methods can perform other operations on the passed in expected value or value to be updated based on Lambda expression before CAS operation. In other words, it is simpler to perform CAS update after additional modification on the expected value or value to be updated.
3.3. Array type atomic operation class
When we first came into contact with the array studied in Java, there will be thread safety problems when we operate the elements. In fact, the meaning of our array type atomic operation class is very simple, which means to update the elements in the array in the form of atoms, so as to avoid thread safety problems. The array type atomic operation classes in the JUC package are specifically divided into the following three classes:
- AtomicIntegerArray: atomically updates elements in an integer array
- AtomicLongArray: atomically updates elements in long integer arrays
- AtomicReferenceArray: atomic updates the elements in the reference type array
Similarly, our analysis only analyzes a single class, and the other two are roughly the same. Here we have analyzed AtomicIntegerArray for example. First, the previous Demo case:
public class AtomicIntegerArrayDemo { static AtomicIntegerArray atomicArr = new AtomicIntegerArray(5); public static class incrementTask implements Runnable{ public void run(){ // Execute the self increment operation of elements in the array. The parameter is index, that is, the array subscript for(int i = 0; i < 10000; i++) atomicArr.getAndIncrement(i % atomicArr.length()); } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[5]; for(int i = 0; i < 5;i++) threads[i] = new Thread(new incrementTask()); for(int i = 0; i < 5;i++) threads[i].start(); for(int i = 0; i < 5;i++) threads[i].join(); System.out.println(atomicArr); /* Execution results: [10000, 10000, 10000, 10000, 10000] */ } }
In the above Demo, we started five threads to increment the elements in the array, and the execution results met our expectations. Next, we will analyze the internal implementation logic of AtomicIntegerArray step by step. The source code is as follows:
public class AtomicIntegerArray implements java.io.Serializable { //Get Unsafe instance object private static final Unsafe unsafe = Unsafe.getUnsafe(); //From the previous analysis of the Unsafe class, we know that the function of arrayBaseOffset() is to obtain the memory starting address of the first element of the array private static final int base = unsafe.arrayBaseOffset(int[].class); private static final int shift; //Internal array private final int[] array; static { //Gets the memory space occupied by an element in the array int scale = unsafe.arrayIndexScale(int[].class); //Judge whether it is the power of 2. It is generally the power of 2. Otherwise, throw an exception if ((scale & (scale - 1)) != 0) throw new Error("data type scale not a power of two"); // shift = 31 - Integer.numberOfLeadingZeros(scale); } private long checkedByteOffset(int i) { if (i < 0 || i >= array.length) throw new IndexOutOfBoundsException("index " + i); return byteOffset(i); } //Calculates the memory address of each element in the array private static long byteOffset(int i) { return ((long) i << shift) + base; } //Omit code }
In our previous Unsafe analysis, we know that arrayBaseOffset can obtain the memory starting position of the first element in an array, and we also have another API: arrayIndexScale, which can know the memory occupied space of the specified subscript element in the array. At present, AtomicIntegerArray is int, When we learn the basics of Java, we know that the size of int occupies 4Byte (bytes) in memory, so the value of scale is 4. Now, what if we calculate the memory starting address of each element in the array according to these two information? With a little derivation, we can draw the following conclusions:
Starting memory location of each element of the array = starting location of the first element of the array + subscript of array element * memory space occupied by each element in the array
The byteOffset(i) method in the above source code is the embodiment of the formula described above. Some people may see some doubts here. The implementation of this method seems to be different from the conclusion I just described. Don't worry. Let's take a look at the value of shift:
shift = 31 - Integer.numberOfLeadingZeros(scale);
Integer.numberOfLeadingZeros(scale): calculate the number of leading zeros of scale (the number of consecutive zeros in front after conversion to binary is called the first zero derivative), scale=4, and when converted to binary is 00000000 00000000 00000100, so the first zero derivative is 29, so the shift value is 2.
So our first question: how to calculate the starting memory location of each element in the array? We use our just obtained shift value to fit into the byteOffset(i) method to calculate the starting memory location of each element in the array (when the array subscript does not exceed the boundary):
byteOffset method body: (i < < shift) + base (i is the index, that is, the subscript of the array element, and base is the starting memory location of the first element of the array)
The first element: memoryaddress = 0 < < 2 + base, that is, memoryAddress = base + 0 * 4
The second element: memoryaddress = 1 < < 2 + base, that is, memoryAddress = base + 1 * 4
The third element: memoryaddress = 2 < < 2 + base, that is, memoryAddress = base + 2 * 4
The fourth element: memoryaddress = 3 < < 2 + base, that is, memoryAddress = base + 3 * 4
Other element positions, and so on
The principle of AtomicIntegerArray.byteOffset method is described above. Therefore, the byteOffset(int) method can calculate the memory address of each element according to the array index. Other methods in AtomicIntegerArray are implemented by indirectly calling CAS atomic operation methods of Unsafe class. See several common methods briefly as follows:
//Execute the auto increment operation and return the old value. The input parameter i is index, that is, the subscript of the array element public final int getAndIncrement(int i) { return getAndAdd(i, 1); } //Specifies that the subscript element performs a self increment operation and returns a new value public final int incrementAndGet(int i) { return getAndAdd(i, 1) + 1; } //Specifies that the subscript element performs a subtraction operation and returns a new value public final int decrementAndGet(int i) { return getAndAdd(i, -1) - 1; } //Indirectly call the unsafe.getAndAddInt() method public final int getAndAdd(int i, int delta) { return unsafe.getAndAddInt(array, checkedByteOffset(i), delta); } //The getAndAddInt method in the Unsafe class performs CAS operations public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
The principles of AtomicReferenceArray and AtomicLongArray are not described.
3.4. Attribute update atomic operation class
If we only need a field (class attribute) of a class to become an atomic operation, that is, a common variable can also become an atomic operation, we can use the attribute to update the atomic operation class. For example, the project requirements change at some time, so that a property in a class that does not need to involve multi-threaded operation needs to perform multi-threaded operation, Because this attribute is used in many places during coding, the code is relatively intrusive and complex to change, and thread safety does not need to be considered where this attribute is used in the previous code. As long as the novelty scenario needs to be guaranteed, the atomic updater can be used to deal with this scenario. The J.U.C.Atomic concurrent atomic package provides the following three classes:
- AtomicIntegerFieldUpdater: an atomic operation class that updates the properties of an integer
- AtomicLongFieldUpdater: updates the atomic operation class of long integer attributes
- AtomicReferenceFieldUpdater: updates the atomic operation class of the attribute in the reference type
However, it should be noted that the conditions for using atomic update classes are relatively harsh, as follows:
- The field of the operation cannot be modified by static
- The field of the operation cannot be modified by final because constants cannot be modified
- The field of the operation must be modified by volatile to ensure visibility, that is, it is necessary to ensure that the reading of data is visible to the thread
- Property must be visible to the region where the current Updater is located. If the atomic Updater operation is not performed inside the current class, the private and protected modifiers cannot be used. When a subclass operates on the parent class, the modifier must have protected permission or above. If it is in the same package, it must have default permission or above. That is, the visibility between the operation class and the operated class should be guaranteed at any time.
Take a simple Demo and feel it:
public class AtomicIntegerFieldUpdaterDemo{ // Atomic operation class that defines the attribute of the update integer. Target attribute: Course.courseScore static AtomicIntegerFieldUpdater<Course> courseAIFU = AtomicIntegerFieldUpdater.newUpdater(Course.class, "courseScore"); // Defines the atomic operation class that updates the properties in the reference type. Target property: Student.studentName static AtomicReferenceFieldUpdater<Student,String> studentARFU = AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"studentName"); // Define the accuracy of atomic counter validation data public static AtomicInteger courseScoreCount = new AtomicInteger(0); public static void main(String[] args) throws Exception{ final Course course = new Course(); Thread[] threads = new Thread[1000]; for(int i = 0;i < 1000;i++){ threads[i] = new Thread(()->{ if(Math.random()>0.6){ courseAIFU.incrementAndGet(course); courseScoreCount.incrementAndGet(); } }); threads[i].start(); } for(int i = 0;i < 1000;i++) threads[i].join(); System.out.println("Course.courseScore: " + course.courseScore); System.out.println("Data validation results:" + courseScoreCount.get()); // Update the demo of the atomic operation class that references the attributes in the type Student student = new Student(1,"Bamboo"); studentARFU.compareAndSet(student,student.studentName,"panda"); System.out.println(student.toString()); } public static class Course{ int courseId; String courseName; volatile int courseScore; } public static class Student{ int studentId; volatile String studentName; public Student(int studentId,String studentName){ this.studentId = studentId; this.studentName = studentName; } @Override public String toString() { return "Student[studentId="+studentId+"studentName="+studentName+"]"; } } } /** Operation results: * Course.courseScore: 415 * Data validation results: 415 * Student[studentId=1studentName=[panda] **/
From the above code, there are two internal static classes: Course and Student. We use AtomicIntegerFieldUpdater to act on the Course.courseScore attribute and AtomicReferenceFieldUpdater to act on Student.studentName. In the above code, we open 1000 threads for logical operations and use AtomicInteger to define the counter courseScoreCount for data validation. The purpose is to verify whether AtomicIntegerFieldUpdater can ensure thread safety. When the random number of the thread we started is greater than 0.6, it means that the score is qualified. At this time, enter the score into Course.courseScore, and then increase the courseScoreCount to facilitate subsequent data validation. The final output of Course.courseScore is consistent with the result of courseScoreCount, which means that AtomicIntegerFieldUpdater can correctly update the Course.courseScore value and ensure thread safety. For AtomicReferenceFieldUpdater, we simply wrote a demo in the above code to demonstrate its use. When we use AtomicReferenceFieldUpdater, it is worth noting that we need to pass in two generic parameters, one is the class object of the modified class and the other is the class object of the modified Field. As for the other AtomicLongFieldUpdater, it will not be demonstrated, and the use method is roughly the same as that of AtomicIntegerFieldUpdater. So far, let's study the implementation principle of AtomicIntegerFieldUpdater. First, the code:
public abstract class AtomicIntegerFieldUpdater<T> { public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName){ return new AtomicIntegerFieldUpdaterImpl<U> (tclass, fieldName, Reflection.getCallerClass()); } }
It is not difficult to find from the source code that AtomicIntegerFieldUpdater is actually defined as an abstract class, and the final implementation class is AtomicIntegerFieldUpdaterImpl. Follow up AtomicIntegerFieldUpdaterImpl:
private static class AtomicIntegerFieldUpdaterImpl<T> extends AtomicIntegerFieldUpdater<T> { // Get unsafe instance private static final Unsafe unsafe = Unsafe.getUnsafe(); // Define memory offset private final long offset; private final Class<T> tclass; private final Class<?> cclass; // Construction method AtomicIntegerFieldUpdaterImpl(final Class<T> tclass, final String fieldName, final Class<?> caller) { final Field field;// Fields to modify final int modifiers;// Field modifier try { field = AccessController.doPrivileged( new PrivilegedExceptionAction<Field>() { public Field run() throws NoSuchFieldException { // Get field object by reflection return tclass.getDeclaredField(fieldName); } }); // Get field modifier modifiers = field.getModifiers(); //Check the access permission of the field and do not throw exceptions within the access range sun.reflect.misc.ReflectUtil.ensureMemberAccess( caller, tclass, null, modifiers); // Get the class loader of the corresponding class object ClassLoader cl = tclass.getClassLoader(); ClassLoader ccl = caller.getClassLoader(); if ((ccl != null) && (ccl != cl) && ((cl == null) || !isAncestor(cl, ccl))) { sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass); } } catch (PrivilegedActionException pae) { throw new RuntimeException(pae.getException()); } catch (Exception ex) { throw new RuntimeException(ex); } Class<?> fieldt = field.getType(); // Determine whether it is of type int if (fieldt != int.class) throw new IllegalArgumentException("Must be integer type"); // Judge whether it is modified by volatile if (!Modifier.isVolatile(modifiers)) throw new IllegalArgumentException("Must be volatile type"); this.cclass = (Modifier.isProtected(modifiers) && caller != tclass) ? caller : null; this.tclass = tclass; // Get the offset of the field in the object memory. You can get or modify the value of the field through the memory offset offset = unsafe.objectFieldOffset(field); } }
From the source code of AtomicIntegerFieldUpdaterImpl, it is not difficult to see that the field updater is actually implemented through the reflection mechanism + Unsafe class. Take a look at the implementation of the self increasing method incrementAndGet in AtomicIntegerFieldUpdaterImpl:
public int incrementAndGet(T obj) { int prev, next; do { prev = get(obj); next = prev + 1; // Call the following compareAndSet method to complete the cas operation } while (!compareAndSet(obj, prev, next)); return next; } // The final call is completed by the compareAndSwapInt() method in the Unsafe class public boolean compareAndSet(T obj, int expect, int update) { if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj); return unsafe.compareAndSwapInt(obj, offset, expect, update); }
We can find that the final implementation of Atomic classes in the Atomic package in J.U.C is completed through the Unsafe class we analyzed earlier, including many concurrency knowledge we will talk about later, such as AQS, which will involve the CAS mechanism.
4, Problems with CAS NO lock
4.1 ABA problem of CAS
When the first thread performs CAS(V,E,N) operation, before obtaining the current variable V and preparing to modify it to the new value N, the other two threads have continuously modified the value of variable V twice, so that the value returns to the original value seen by the first thread. In this case, we cannot correctly judge whether this variable has been modified. For example, threads T1,T2,T3, shared variable atomicI=0, When the first thread T1 is ready to change atomicI to 1, the value T1 sees is 1 and reads atomicI=1 back to the working memory. However, during the operation of T1 thread, T2 thread updates the value to 1, and then T3 thread changes atomicI to 0. At this time, T1 cannot know that this value has been changed by other threads when it comes back to update, When V==E is judged, if the atomicI is still the original value, the atomicI will be changed. However, the field at this time is different from the field when T1 thread first sees the value. As shown below:
The above example is the problem of CAS mechanism. In general, the probability of occurrence is very small, and even if there is a general business, it will not cause any problems. For example, in the above example, even if there is an ABA problem, it will not affect the final consistency of data, because the expected value of T1 thread itself is 1, even if there is an ABA problem, The updated value of T1 thread is still 1. However, in some cases, such problems need to be avoided. For example, the stack stack implemented by a one-way linked list is as follows:
As shown in the above figure, at this time, the top element of the stack is a. at this time, thread T1 already knows that A.next is element B. it wants to replace the top element of the stack with B through CAS mechanism. The execution is as follows:
stackHead.compareAndSet(A,B);
When thread T1 executes this line of code, thread T2 intervenes. Thread T2 takes A and B out of the stack, and then puts elements Y, X and A into the stack. At this time, elements Y, X and A are in the stack and element B is in an isolated state. The schematic diagram is as follows:
Just at this time, thread T1 performs CAS writeback operation. It is found that the top of the stack is still A, so CAS succeeds and the top of the stack becomes B, but actually B.next is null, so the situation at this time becomes:
Among them, there are only B elements in the stack, and Y and X elements no longer exist in the stack. However, T2 thread does perform y and X stacking operations, but it loses X and y for no reason.
In short, as long as there are operations that need to be made based on dynamic changes in the scene, the ABA problem needs to be solved. However, if your application only makes judgments based on the results obtained on the data surface, you can not pay attention to the ABA problem.
4.2 solution to ABA problem of CAS
So when we have such problems and need our attention, how to solve them? When we implement optimistic locks in other ways, we usually mark them by version number to avoid the problems caused by concurrency. There are two main solutions to the ABA problem of CAS in Java:
- AtomicStampedReference: timestamp control, which can be completely solved
- AtomicMarkableReference: maintain boolean value control, which cannot be completely eliminated
4.2.1,AtomicStampedReference
AtomicStampedReference is an object reference with a timestamp. The data and timestamp are stored internally in the form of wrapped Pair object key value pairs. During each update, the data itself and timestamp are compared first. Only when both meet the expected values, Unsafe's compareAndSwapObject method is called for writing. Of course, the AtomicStampedReference not only sets the new value, but also records the timestamp of the change. This solves the ABA problem caused by CAS mechanism before. Simple cases are as follows:
public class ABAIssue { // Define atomic counter, initial value = 100 private static AtomicInteger atomicI = new AtomicInteger(100); // Define AtomicStampedReference: an initial value and initial time need to be passed in during initialization private static AtomicStampedReference<Integer> asRef = new AtomicStampedReference<Integer>(100, 0); /** * Unused AtomicStampedReference thread group: TA TB */ private static Thread TA = new Thread(() -> { System.err.println("not used AtomicStampedReference Thread group:[TA TB] >>>>"); // The updated value is 101 boolean flag = atomicI.compareAndSet(100, 101); System.out.println("thread TA: 100 -> 101.... flag:" + flag + ",atomicINewValue: " + atomicI.get()); // The updated value is 100 flag = atomicI.compareAndSet(101, 100); System.out.println("thread TA: 101 -> 100.... flag:" + flag + ",atomicINewValue: " + atomicI.get()); }); private static Thread TB = new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } boolean flag = atomicI.compareAndSet(100, 888); System.out.println("thread TB: 100 -> 888.... flag:" + flag + ",atomicINewValue: " + atomicI.get() + "\n\n"); }); /** * Using AtomicStampedReference thread group: T1 T2 */ private static Thread T1 = new Thread(() -> { System.err.println("use AtomicStampedReference Thread group:[T1 T2] >>>>"); // The updated value is 101 boolean flag = asRef.compareAndSet(100, 101, asRef.getStamp(), asRef.getStamp() + 1); System.out.println("thread T1: 100 -> 101.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... current Time: " + asRef.getStamp()); // The updated value is 100 flag = asRef.compareAndSet(101, 100, asRef.getStamp(), asRef.getStamp() + 1); System.out.println("thread T1: 101 -> 100.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... current Time: " + asRef.getStamp()); }); private static Thread T2 = new Thread(() -> { int time = asRef.getStamp(); System.out.println("Before thread hibernation Time Value:" + time); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } boolean flag = asRef.compareAndSet(100, 888, time, time + 1); System.out.println("thread T2: 100 -> 888.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... current Time: " + asRef.getStamp()); }); public static void main(String[] args) throws InterruptedException { TA.start(); TB.start(); TA.join(); TB.join(); T1.start(); T2.start(); } } /** * Unused AtomicStampedReference thread group: [TA TB] > > > > * Thread TA: 100 - > 101.... flag: true, atomicinewvalue: 101 * Thread TA: 101 - > 100.... flag: true, atomicinewvalue: 100 * Thread TB: 100 - > 888.... flag: true, atomicinewvalue: 888 * * * Using AtomicStampedReference thread group: [T1, T2] > > > > * Time value before thread hibernation: 0 * Thread T1:100 - > 101.... flag: true.... asrefnewvalue: 101.... current Time: 1 * Thread T1:101 - > 100.... flag: true.... asrefnewvalue: 100.... current Time: 2 * Thread T2:100 - > 888.... flag: false.... asrefnewvalue: 100.... current Time: 2 */
By observing the test results of AtomicInteger and AtomicStampedReference in the above Demo, we can know that AtomicStampedReference can indeed solve the ABA problem we described earlier. How does AtomicStampedReference solve the ABA problem? Let's look at its internal implementation:
public class AtomicStampedReference<V> { // Store data and timestamp through Pair inner 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); } } // An internal class that stores values and times private volatile Pair<V> pair; // Construction method: initial value and time initial value shall be passed in during initialization public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); } }
compareAndSet method source code implementation:
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))); } // The final implementation calls the compareAndSwapObject() method in the Unfase class private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); }
In the source code implementation of the compareAndSet method, it is not difficult to find that it compares the current data and the current time at the same time. Only if both are equal will the casPair() method be executed, and the casPair() method is also an atomic method. The final implementation calls the compareAndSwapObject() method in the Unsafe class.
4.2.2,AtomicMarkableReference
AtomicMarkableReference is different from the AtomicStampedReference we discussed earlier. AtomicMarkableReference can only reduce the occurrence of ABA problems to a certain extent, but it can not completely eliminate ABA problems. Because AtomicStampedReference internally maintains a Boolean ID, it means that unlike the timestamp maintained in AtomicStampedReference before, the value will increase continuously. Because AtomicStampedReference internally maintains a boolean, it means that it will only switch back and forth between true and false, Therefore, there is still the concept of ABA problem. Let's start with a demo:
public class ABAIssue { static AtomicMarkableReference<Integer> amRef = new AtomicMarkableReference<Integer>(100, false); private static Thread t1 = new Thread(() -> { boolean mark = amRef.isMarked(); System.out.println("thread T1: Flag before modification Mrak:" + mark + "...."); // Update the value to 200 System.out.println("thread T1: 100 --> 200.... Return value after modification Result:" + amRef.compareAndSet(amRef.getReference(), 200, mark, !mark)); }); private static Thread t2 = new Thread(() -> { boolean mark = amRef.isMarked(); System.out.println("thread T2: Flag before modification Mrak:" + mark + "...."); // Update value back to 100 System.out.println("thread T2: 200 --> 100.... Return value after modification Result:" + amRef.compareAndSet(amRef.getReference(), 100, mark, !mark)); }); private static Thread t3 = new Thread(() -> { boolean mark = amRef.isMarked(); System.out.println("thread T3: Pre sleep flag Mrak:" + mark + "...."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } boolean flag = amRef.compareAndSet(100, 500, mark, !mark); System.out.println("thread T3: 100 --> 500.... flag:" + flag + ",newValue:" + amRef.getReference()); }); public static void main(String[] args) throws InterruptedException { t1.start(); t1.join(); t2.start(); t2.join(); t3.start(); /** * The output results are as follows: * Thread T1: flag before modification Mrak:false * Thread T1:100 -- > 200.... return value after modification Result:true * Thread T2: Flag Mrak:true before modification * Thread T2:200 -- > 100.... return value after modification Result:true * Thread T3: pre sleep flag Mrak:false * Thread T3:100 -- > 500.... flag: true, newvalue: 500 */ /* t3 After the thread is executed, the result is successfully updated to 500, representing t1 and t2 The modifications made by the thread are still invisible to the t3 thread */ } }
From the above demo, we can find that ABA problems still occur after using AtomicMarkableReference, which does not really avoid the occurrence of ABA problems. Therefore, we will not elaborate on the principle of AtomicMarkableReference (it is roughly similar to the AtomicMarkableReference analyzed earlier, and interested partners can study it by themselves ~).
From the previous discussion on ABA problems, we know that AtomicMarkableReference can reduce the probability of ABA problems to a certain extent, but it can not completely eliminate them. If we need to completely avoid ABA problems, we still need to use atomicmarkedreference.
5, On CAS unlocked spin
At the beginning of this article, we talked about the preliminary concept of no lock. In fact, the concept of no lock was in our last article: Analysis of the implementation principle of Synchronized keyword The lock upgrade expansion process is also mentioned many times. Lock free is also called spin lock in some places. Spin is a situation where if there is thread competition, but the logical threads here execute very fast. When some threads that do not obtain resources (values, etc.) can also obtain resources and execute in the near future, the OS allows the threads that do not obtain resources to perform several empty loop operations and wait for resources. The spin operation can indeed improve efficiency, because the spin lock only blocks the thread logically and stops the thread from executing in the user state, and does not really suspend the corresponding kernel thread in the kernel state, which can reduce the resources consumed by many kernel hanging / dropping threads. However, the problem is that when there are more and more threads and the competition is fierce, the longer the CPU consumption will lead to a sharp decline in performance. Therefore, the Java virtual machine generally has a certain number of spin locks. It may give up after 50 or 100 cycles, and directly let the OS suspend the kernel threads to give up CPU resources.
6, References and books
- Deep understanding of JVM virtual machines
- The beauty of Java Concurrent Programming
- Java high concurrency programming
- Core technology of 100 million traffic website architecture
- Java Concurrent Programming Practice