Write in front:
The content of this article is summarized through personal arrangement and reference to relevant materials, and some mistakes will inevitably occur
If there is an error, please point it out in the comments! thank you
1 CAS
We usually familiar with lock concurrency is the typical representative of the synchronize, concurrent execution process through the keyword can control one and only one thread can access to a Shared resource, its principle by the current thread holds the lock the current object, thus have access, and other don’t hold the thread of object lock cannot have access, This ensures thread safety
But we’re going to look at another concurrency strategy that works the other way around: lockless concurrency, which ensures program concurrency without locking
1.1 no lock
When talking about the concept of no lock, optimists and pessimists are always associated. For optimists, they believe that things will always develop in a good direction, and they always think that the probability of bad situation is very low, so they can do things without hesitation. But for the pessimists, they always think that if the development is not controlled in time, it will be irreversible, even if the irreversible situation is almost impossible to happen
The mapping of these two factions to concurrent programming is analogous to locking and no-lock strategies, in that locking is a pessimistic strategy and no-lock is an optimistic strategy. Because locked concurrent programs assume that a conflict will occur every time they access a shared resource, they must implement a locking policy for every data operation. However, without lock, it is always assumed that there is no conflict of access to the shared resource, and the thread can keep executing without locking or waiting. Once a conflict occurs, the no-lock policy adopts a CAS technology to ensure the security of thread execution
1.2 Unlocked executor -CAS
1.2.1 the CAS
The full name of CAS is Compare And Swap, And the core idea of its algorithm is:
Execute function: CAS(V,E,N)
There are three parameters:
- V represents the variable to be updated
- E is the expected value
- N indicates the new value
If the value of V is equal to the value of E, set the value of V to N. If the value of V is different from the value of E, it indicates that the update has been made by another thread, and the current thread does not change
When the value of the expected value is the same as the variable value of the current thread, it indicates that no thread has operated on the value. The current thread can modify the value, and then the CAS operation can be performed. If the expected value is different from the variable value of the current thread, it indicates that the value has been modified by another thread. In this case, the update operation is not performed, but the variable can be read again and try to modify the variable again, or the operation can be abandoned. The schematic diagram is as follows:
Because the CAS operation are optimists, it always think they can successfully complete the operation, when multiple threads at the same time use the CAS operation when a variable, only one will win out, and updated successfully, the others all will fail, but no thread failure will be hung, only to be told that failure, and allowed to try again, of course also allow threads to give up operation failure, This can also be seen in the picture
Based on this principle, even if the CAS operation does not have a lock, it is also aware of the impact of other threads on the operation of shared resources and performs corresponding processing measures. It can also be seen that since there is no lock in the lock-free operation, it is impossible for the CAS to be deadlocked, that is, CAS is inherently immune to deadlocks
1.2.2 CAS Support by CPU Instructions
Maybe we have such a question, assuming that there are multiple threads performing CAS operation and there are many CAS steps, is it possible that when V and E are judged to be the same, they switch to another thread and modify the value, resulting in inconsistent data?
The answer is no, because CAS is a system primitive, which belongs to the language of the operating system and is composed of several instructions to complete a process of a certain function. Moreover, the execution of the primitive must be continuous and cannot be interrupted during the execution
This means that CAS is an atomic instruction of the CPU and does not cause the so-called data inconsistency problem
1.3 Unsafe Class (Pointers)
Unsafe classes exist in the Sun. misc package, and their internal method operations can operate directly on memory like Pointers to C. The Unsafe class is not safe from its name alone. After all, the Unsafe class has pointer operations similar to C, and therefore should never be used in the first place
The Unsafe class itself is not recommended, but it’s important to know about it because CAS operations depend on the Unsafe class’s methods. Note that all its methods are native modifications, meaning that its methods call underlying operating system resources directly to perform tasks. The key features of the Unsafe class are as follows:
Memory management, the Unsafe class has methods to manipulate memory directly
// Allocate memory Specifies the size of memory
public native long allocateMemory(long bytes);
// Reallocate memory of the specified size based on the given memory address setting
public native long reallocateMemory(long address, long bytes);
// Used to free memory applied by allocateMemory and reallocateMemory
public native void freeMemory(long address);
// Sets all bytes in the memory block at the given offset of the specified object to fixed values
public native void setMemory(Object o, long offset, long bytes, byte value);
// Set the value of the given memory address
public native void putAddress(long address, long x);
// Get the value of the specified memory address
public native long getAddress(long address);
// Set the long value of the given memory address
public native void putLong(long address, long x);
// Get the long value of the specified memory address
public native long getLong(long address);
// Sets or gets a byte value for the specified memory
public native byte getByte(long address);
public native void putByte(long address, byte x);
/ / other basic data types (long, char, and float, double, short, etc.) at the same operation with putByte and getByte
// The memory page size of the operating system
public native int pageSize(a);
Copy the code
Provides a new approach to instance objects
// Pass in the class of an object and create the instance object, but the constructor is not called
public native Object allocateInstance(Class cls) throws InstantiationException;
Copy the code
Class and instance objects and variables are operated in the following main ways
// Get the offset of field F in the instance object
public native long objectFieldOffset(Field f);
// Static property offset, used to read and write static properties in the corresponding Class object
public native long staticFieldOffset(Field f);
// Return f.declaringClass ()
public native Object staticFieldBase(Field f);
// Get the int value at the offset of the given object. The offset is simply a pointer to the memory address of the variable.
// The offset can be used to obtain the variable of the object 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);
// Get the value of the reference type at the offset of the given object
public native Object getObject(Object o, long offset);
// Sets the value of the reference type at the offset of the given object
public native void putObject(Object o, long offset, Object x);
/ / other basic data types (long, char, byte, float, double) at the same operation with getInthe and putInt
// Set the int value of the given object, using volatile semantics, that is, immediately updated to memory to be visible to other threads
public native void putIntVolatile(Object o, long offset, int x);
// Use volatile semantics to always obtain the latest int value.
public native int getIntVolatile(Object o, long offset);
/ / other basic data types (long, char, byte, float, double) operation with putIntVolatile and getIntVolatile, same reference type putObjectVolatile, too.
As with putIntVolatile, but the field being operated on must be volatile
public native void putOrderedInt(Object o,long offset,int x);
Copy the code
Operate on an array
// Get the offset address of the first element of the array
public native int arrayBaseOffset(Class arrayClass);
// The memory space occupied by an element in an array. ArrayBaseOffset is used with arrayIndexScale to locate each element in the array
public native int arrayIndexScale(Class arrayClass);
Copy the code
Thread suspend and resume operations
Suspending a thread is implemented through the park method. After calling park, the thread will remain blocked until timeout or interruption conditions occur. Unpark can terminate a suspended thread and restore it to normal. The suspension of Java threads is encapsulated in the LockSupport class, which has various versions of the pack methods, and the underlying implementation uses the unsafe.park () and unsafe.unpark () methods
// The thread calls this method and blocks until it times out or an interrupt condition occurs.
public native void park(boolean isAbsolute, long time);
Java.util.concurrent the suspension operations in the java.util.concurrent package are implemented in the LockSupport class, which uses these two methods at the bottom.
public native void unpark(Object thread);
Copy the code
So let’s introduce suspension of the operating system
Pending state
//TODO
1.3.1 Testing the Unsafe class
public class UnsafeDemo {
public static void main(String[] args) throws Exception {
// get the Field object corresponding to theUnsafe class theUnsafe attribute by reflection
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// Make the Field accessible
field.setAccessible(true);
// Get the object from the Field. Null is passed because the Field is static
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe.toString());
// Create objects directly through allocateInstance
User user = (User) unsafe.allocateInstance(User.class);
Class userClass = user.getClass();
Field name = userClass.getDeclaredField("name");
Field age = userClass.getDeclaredField("age");
Field id = userClass.getDeclaredField("id");
// Get the offset of the instance variables name and age in the object memory and set the value
unsafe.putInt(user,unsafe.objectFieldOffset(age),18);
unsafe.putObject(user,unsafe.objectFieldOffset(name),"hello jiang");
// this returns user.class
Object staticBase = unsafe.staticFieldBase(id);
System.out.println("staticBase:" + staticBase);
// Get the offset of the static variable id staticOffset
long staticOffset = unsafe.staticFieldOffset(id);
// Get the value of a static variable
System.out.println("ID before setting :" + unsafe.getObject(staticBase,staticOffset));
/ / set the value
unsafe.putObject(staticBase,staticOffset,"jiang xiaopang");
// Get the value of a static variable
System.out.println("设置后的ID:" + unsafe.getObject(staticBase,staticOffset));
long data = 1000;
byte size = 1;
// call allocateMemory to allocateMemory and get memoryAddress memoryAddress
long memoryAddress = unsafe.allocateMemory(size);
// Write data directly to memory
unsafe.putAddress(memoryAddress,data);
// Get data for the specified memory address
long addrData = unsafe.getAddress(memoryAddress);
System.out.println("addData:"+ addrData); }}@ToString
class User {
private String name;
private int age;
private static String id="USER_ID";
public User(a){
System.out.println("User constructor called"); }}Copy the code
The output
Sun. Misc. Unsafe @ 14 ae5a5 staticBase: class org. Jiang. TestUnsafe. User ID before setting: USER_ID setting after ID: jiang xiaopang addData: 1000Copy the code
Although the getUnsafe() method exists in the Unsafe class, it is only available for use by the advanced Bootstrap classloader. Calls to it by ordinary users will throw exceptions, So in the Demo, we’re using reflection to get the Unsafe instance object and to do that.
public static Unsafe getUnsafe(a) {
Class cc = sun.reflect.Reflection.getCallerClass(2);
if(cc.getClassLoader() ! =null)
throw new SecurityException("Unsafe");
return theUnsafe;
}
Copy the code
1.3.2 Unsafe Indicates CAS operations
CAS are instructions that are directly supported by the CPU, namely the lock-free operations we examined earlier. Lock-free operations in Java are implemented based on the following three methods (the Atomic family of internal methods explained later)
// The first parameter o is the given object, offset is the memory offset of the object, by which the field is quickly located and the value of the field is set or obtained.
// Expected indicates the expected value, and x indicates the value to be set. The following three methods all operate via 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);
Copy the code
The Broadening class also introduces several new Java8 methods, which are implemented based on the CAS method described above:
// add a delta to the object o.
// This is a CAS operation that exits the loop until it is successfully set and returns the old value
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// Get the latest value in memory
v = getIntVolatile(o, offset);
// The operation is performed through CAS
} while(! compareAndSwapInt(o, offset, v, v + delta));return v;
}
// add the same function as above, except that this operation operates on long data
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;
}
// set the memory offset to newValue.
// This is a CAS operation that exits the loop until it is successfully set and returns the old value
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;
}
// add a new type of long
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 add, same as above, operate 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;
}
Copy the code
The above methods are still visible in Atomic series analysis
1.3.3 analysis based on Unsafe#getAndAddInt
Above we have introduced the source code of the Unsafe#getAndAddInt method
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// Get the latest value in memory
v = getIntVolatile(o, offset);
// The operation is performed through CAS
} while(! compareAndSwapInt(o, offset, v, v + delta));return v;
}
Copy the code
We create a local variable v, use getIntVolatile() to obtain the offset value of o, and then call compareAndSwapInt() to make constant comparisons
We know that the compareAndSwapInt() method checks whether the object o’s offset is still v. If it is v, it swaps v + delta and returns true, and vice versa
If false is returned, continue the loop, change variable V to the value of the latest object O, whose offset is offset, and then judge
So as long as other threads make changes, the loop continues to execute and get the value of the latest property until no other threads make changes
1.4 Atomic series
Unsafe, unlockable CAS, and its pointer class unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. unsafe. Unsafe Java is available from JDK1.5. Util. Concurrent. Atomic package, in the package provides many atomic operations based on CAS implementation class, mainly divided into four types
1.4.1 Atomic update base types
The atomic update basic types mainly include three classes:
- AtomicBoolean: Atom updates Boolean type
- AtomicInteger: Atom update integer
- AtomicLong: Atom updates long integers
The realization principle and use of these three classes are almost the same, here to AtomicInteger as an example for analysis, AtomicInteger mainly for int type data to perform atomic operations, it provides atomic increment, atomic subtraction and atomic assignment methods, the following first look at the source code
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// Get the pointer class Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
// The memory offset of the following variable value within the AtomicInteger instance object
private static final long valueOffset;
static {
try {
// Retrieve the offset of the value variable in the object memory using the unsafe class objectFieldOffset() method
// With valueOffset, the unsafe class's internal method can retrieve the value variable to evaluate or assign to
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw newError(ex); }}// The current AtomicInteger encapsulates the int variable value
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger(a) {}// Get the current latest value,
public final int get(a) {
return value;
}
// Set the current value, volatile, and final to further ensure thread-safety.
public final void set(int newValue) {
value = newValue;
}
// Will eventually be set to newValue, which may cause other threads to retrieve the old value for a short time later, similar to lazy loading
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
Unsafe.com pareAndSwapInt(); // Set the new value and get the old value
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
// If the current value is expect, 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);
}
// Add 1 to the current value to return the old value, underlying CAS operation
public final int getAndIncrement(a) {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// If the current value is reduced by 1, the old value is returned
public final int getAndDecrement(a) {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
// Add delta to current value, return old value, bottom CAS operation
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// Add 1 to the current value, return the new value, underlying CAS operation
public final int incrementAndGet(a) {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// If the current value is reduced by 1, the new value is returned
public final int decrementAndGet(a) {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
// Add delta to current value, return new value, bottom CAS operation
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
// omit some unusual methods....
}
Copy the code
So what did AtomicInteger do in the first place?
The instance object in the pointer class Unsafe is for pointer operations
2. We know that the value attribute in AtomicInteger is actually the initial value parameter passed in when the AtomicInteger object is created, that is, the encapsulated int value
3. Obtain the memory offset address of the value attribute in the AtomicInteger object through a static code block during initialization
Now, how are the main methods of doing operations implemented?
The getAndSet(), compareAndSet(), and getAndAdd() methods are actually cas-related operations called from the Unsafe class, indicating that AtomicInteger is lock-free
GetAndAddInt () = getAndAddInt() = getAndAddInt(
// The getAndAddInt method in the Unsafe class
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;
}
Copy the code
This is a do… While loop, the judgment condition of the loop is! CompareAndSwapInt (o, offset, v, v + delta). In the Unsafe class, we already know that using the variables o and offset, we can get the value of the property defined by the offset. V is the first value we get from the main memory. Then we want to change this value to v + delta
If the value of the main memory has changed, the CAS operation returns a Boolean value of false, and the variable v needs to retrieve the value of the property defined by the specified offset, and the loop continues
Only if the value of main memory does not change and CAS returns a Boolean value of true will the loop be broken out and the value of main memory be changed successfully
This ensures that the modified operation is based on lock-free atomic operation (without considering ABA)
12.4.2 AtomicInteger Method principle analysis
Broadening the Unsafe class, AtomicInteger and AtomicBoolean work in the same way, so we’ll look at AtomicInteger#getAndSet as an example
Let’s start by examining the AtomicInteger constructor. We know that when the constructor is called it initializes the class and executes some static variables and static code blocks (i.e. the CINit method)
The AtomicInteger class maintains a value variable of type int (volatile). The static code block reads:
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
Copy the code
Gets the memory offset address of the value variable and assigns the value to valueOffset
Also, the class maintains a static variable unsafe:
private static final Unsafe unsafe = Unsafe.getUnsafe()
We know that objects cannot be created directly from the constructor in the Unsafe class, so here is an example of getting the Unsafe class
The constructor then assigns the parameter to the value variable
After initialization, let’s look at the getAndSet method
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
Copy the code
As you can see from the Unsafe class, this is actually a call to the getAndSetInt() method, as we’ve already looked at some of the broadening methods above, so the principles are pretty much the same
In fact, it also circulates through the characteristics of CAS and constantly takes out the latest value in main memory, and then compares it with the expected value to determine whether it can be updated
1.4.3 Atom update Reference
AtomicInteger, AtomicBoolean, AtomicBoolean, AtomicInteger, AtomicBoolean, AtomicBoolean, AtomicInteger, AtomicBoolean, AtomicBoolean, AtomicInteger, AtomicBoolean, AtomicBoolean, AtomicInteger, AtomicBoolean, AtomicBoolean, AtomicInteger, AtomicBoolean, AtomicBoolean, AtomicBoolean, AtomicInteger, AtomicBoolean, AtomicBoolean This is where the AtomicReference type, AtomicReference, needs to be used, and the basic principle is similar
Here is a simple example:
Define custom classes for operations:
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person {
private String name;
private int age;
}
Copy the code
Define test classes to test:
@Slf4j
public class Demo7 {
private static AtomicReference<Person> reference = new AtomicReference<>();
public static void main(String[] args) {
Person person = new Person("jiang".18);
reference.set(person);
Person newPerson = new Person("nidi".23);
person.setName("liu");
booleanflag = reference.compareAndSet(person, newPerson); log.info(String.valueOf(flag)); log.info(reference.get().toString()); }}Copy the code
Here generics on the AtomicReference class are reference types, and passing in different generics is essentially atomic operations on objects of different types
Reference. Set (person) is essentially an assignment to a value variable using a constructor similar to the AtomicInteger class
The test results
Since the data was not modified by any other thread, the expected value is the same as the value of the variable in main memory, so the modification succeeds
Test by modifying the expected value
@Slf4j
public class Demo7 {
private static AtomicReference<Person> reference = new AtomicReference<>();
public static void main(String[] args) {
Person person = new Person("jiang".18);
reference.set(person);
Person newPerson = new Person("nidi".23);
Person updatePerson = new Person("liu".18);
booleanflag = reference.compareAndSet(updatePerson, newPerson); log.info(String.valueOf(flag)); log.info(reference.get().toString()); }}Copy the code
The test results
This is essentially simulating that there are other threads making changes to the variable, and you can see that the atomic changes fail
1.4.2.1 ABA problem
We know that CAS is really just a comparison and exchange, so here’s a question to think about:
If a thread in the access to the latest variable values before and after comparison and exchange, other threads on the variable operating twice, once changed to other values, another change back again, so at this time the CAS in comparison and exchange, it found that a variable’s value is the same as the expected value, does this mean no other threads to operate? , the above problems can be described as follows:
12.4.2.2 AtomicStampedReference
The AtomicStampedReference atom class is a time-stamped update of an object reference (essentially a version that has been modified by another thread)
After each change, the AtomicStampedReference not only sets the new value but also records the time stamp of the change
When an AtomicStampedReference object value is set, the object value and timestamp must meet the expected value to be read successfully. Therefore, you cannot predict whether the value has been modified during repeated reads and writes
Test the AtomicStampedReference class:
public class Demo07 {
static AtomicInteger atIn = new AtomicInteger(100);
// Initialization requires passing in an initial value and an initial time
static AtomicStampedReference<Integer> atomicStampedR =
new AtomicStampedReference<Integer>(200.0);
static Thread t1 = new Thread(new Runnable() {
@Override
public void run(a) {
// Update to 200
atIn.compareAndSet(100.200);
// Update to 100
atIn.compareAndSet(200.100); }});static Thread t2 = new Thread(new Runnable() {
@Override
public void run(a) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag=atIn.compareAndSet(100.500);
System.out.println("flag:"+flag+",newValue:"+atIn); }});static Thread t3 = new Thread(new Runnable() {
@Override
public void run(a) {
int time=atomicStampedR.getStamp();
// Update to 200
atomicStampedR.compareAndSet(100.200,time,time+1);
// Update to 100
int time2=atomicStampedR.getStamp();
atomicStampedR.compareAndSet(200.100,time2,time2+1); }});static Thread t4 = new Thread(new Runnable() {
@Override
public void run(a) {
int time = atomicStampedR.getStamp();
System.out.println("T4 time before sleep."+time);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag=atomicStampedR.compareAndSet(100.500,time,time+1);
System.out.println("flag:"+flag+",newValue:"+atomicStampedR.getReference()); }});public static void main(String[] args) throws InterruptedException { t1.start(); t2.start(); t1.join(); t2.join(); t3.start(); t3.join(); t4.start(); t4.join(); }}Copy the code
The basic logic of the test scheme is as follows:
AtomicInteger and AtomicStampedReference are created for comparison
We know that thread T1 must execute first and then put thread T2 in the ready state after completion of execution, so thread T1 will update variable 200, and then update variable 100, and then thread T2 will perform CAS operation
The basic logic is essentially the same for T3 and T4 threads, but the AtomicStampedReference class is added for atomic updates
The test results
You can see here that the AtomicStampedReference class avoids the ABA problem
The AtomicStampedReference class is described below:
The timestamp variable stamp here is a variable of int type, and stamp is not set automatically. Instead, the value of stamp variable is actively updated every time the CAS operation is performed. Of course, if the modification is successful, the version will be changed; otherwise, the version will not be changed
1.4.4 Atomic classes implement spin locking
Spin lock is an assumption that the current thread will acquire the lock in the near future. Therefore, the virtual machine will make the current thread to acquire the lock do several empty loops (this is why it is called spin). After several loops, if the lock is obtained, it will enter the critical region smoothly. If the lock is not available, threads are suspended at the operating system level, which can be a real productivity boost
However, as the number of threads increases and the competition becomes fierce, the CPU usage becomes longer, leading to a sharp decline in performance. Therefore, Java virtual machines generally have a certain number of spin locks, which may be abandoned after 50 to 100 cycles and directly suspend the thread to release CPU resources
AtomicReferenece implements a simple spinlock below:
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock(a) {
Thread currentThread = Thread.currentThread();
while(! sign.compareAndSet(null, currentThread)) {
}
}
public void unlock(a) {
Thread currentThread = Thread.currentThread();
while(! sign.compareAndSet(currentThread,null) {}}}Copy the code
Here the CAS atomic operation is used as the underlying implementation, with the lock() method setting the value to be updated to the current thread and the expected value to NULL
The unlock() function is set to update the value to null, and the expected value is set to the current thread
We then use lock() and unlock() methods to control the opening and closing of a spin lock. Note that this is an unfair lock
In fact, the CAS operation inside the AtomicInteger(or AtomicLong) atomic class is also implemented through a while loop, which ends if the thread successfully updates the values of the values, but is also a type of spin lock
Test custom splock:
public class Demo007 {
private static SpinLock lock = new SpinLock();
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch(InterruptedException e) { e.printStackTrace(); }}finally{ lock.unlock(); }},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new Date());
lock.lock();
System.out.println(new Date());
System.out.println("Lock has been obtained"); }}Copy the code
Here we know that we don't use locks in our custom spin locks, so how do we do this?
We first create a spinlock object
For Thread T1, we call the lock.lock() method first. Since we maintain an AtomicReference
variable sign in our custom SpinLock class, and call the AtomicReference constructor when the class is initialized, Since no arguments are passed in the constructor, the value attribute in the AtomicReference class is null Then, the CAS operation is used to determine whether the current value variable is null. If so, it is changed to the current thread. Obviously, thread T1 has been changed successfully (because the mian thread has been hibernated for 1s).
When the main thread attempts to call lock.lock(), it finds that the CAS value has been modified, so it spins itself
This is how the spin lock is implemented, and the mian thread stops spinning and executes downwards only when thread T1 re-sets the variable value from the current thread to NULL via the CAS operation using lock.unlock()
The test results
You can see from the timing that the main thread starts executing after the T1 thread completes
2 Shared model – immutable
2.1 Issues of date conversion
The SimpleDateFormat class is normally used for date formatting conversions. Is it safe in a concurrent environment?
Here’s a simple test:
public class Demo8 {
private static SimpleDateFormat format = new SimpleDateFormat("YYYY-MM-dd");
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
Date date = format.parse("2021-03-04");
log.info("{}",date);
} catch(ParseException e) { e.printStackTrace(); } }).start(); }}}Copy the code
The test results
Now that this is a problem, how can we improve it?
The DateTimeFormatter class can be used instead of the SimpleDateFormat class
The DateTimeFormatter class is immutable and thread-safe
Here’s the test:
@Slf4j
public class Demo8 {
private static DateTimeFormatter format = DateTimeFormatter.ofPattern("YYYY-MM-dd");
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
TemporalAccessor accessor = format.parse("2021-03-04");
log.info("{}",accessor); }).start(); }}}Copy the code
The test results
13.2 Cause and solution of SimpleDateFormat Thread Insecurity
2.2.1 Reason why SimpleDateFormat thread is unsafe
2.2.2 Solution
2.3 Immutable object design
We are familiar with the String class, so let’s use the String class to design immutable elements
You can see that there are two attributes in the String class, one of which is a char array to hold the array of bytes used to create strings, and the other hash is a hashcode to cache String objects
For the final keyword:
- The final modifier property is used to ensure that the property is only readable, but cannot be modified
- A final class guarantees that methods in that class cannot be overridden, preventing subclasses from inadvertently breaking their immutability (
Classes that are actually final implicitly add the final keyword to their methods by default
), and final modified classes cannot be inherited
Let’s look at the String constructor:
First, let’s understand what this comment means:
The contents of the byte array are copied because the value property in the String is not affected when the original array is modified
Let’s look at the constructor:
The value property is assigned to the byte array by copying the original byte array, which means that any changes to the parameters passed in will not affect the internal structure of the String
Now let’s look at the substring method:
The first step is to check that the truncated length matches the rule, and if it does, we call the String constructor, which we know is actually done by copying an array of bytes
So no matter how the byte array of the string before the interception changes, it doesn't affect the content of the string after the interception, right
In fact, this embodies the concept of immutable object design: protective copy
When a new String is created, a new byte array is generated, essentially copying the original byte array. The idea of creating duplicate objects to avoid sharing problems is protected copying
2.4 Share Mode
In the process of object-oriented design, it is sometimes necessary to create a large number of identical and similar object instances. It takes a lot of performance to create so many similar objects
2.4.1 Definition and Features
define
Sharing technology is used to effectively support the reuse of a large number of fine-grained objects. By sharing existing objects, the number of objects that need to be created can be greatly reduced, and the overhead of a large number of similar classes can be avoided, so as to improve the utilization rate of system resources
advantages
Only one copy of an object is saved, reducing the number of objects in the system and reducing the pressure on the memory caused by fine-grained objects in the system
disadvantages
In order to make objects shareable, some states that cannot be shared need to be externalized, increasing the complexity of the system
Reading the external state of the share mode makes the run time slightly longer
2.4.2 structure
There are two requirements in the definition of the share pattern: fineness and shared objects
Because of the need for fine granularity, there is inevitably a large number of objects with similar properties, so the information about objects needs to be divided into two parts: internal state and external state
- Internal state refers to the information shared by the object, stored within the metadata, and does not change with the environment
- An external state is a tag that an object can depend on, changes with the environment, and is not shareable
For example, the connection object in the connection pool, the user name, password, connection URL and other information stored in the connection object have been set up when the object is created and will not change with the environment. These are internal states
When each connection is to be recycled, we need to mark it as available. These are external states
Primary role in share mode
- Abstract privilege role: is the base class of all concrete privilege classes. To define the common interface that the privilege specification needs to implement, non-privilege external state is injected through methods in the form of parameters
- Concrete user role: Implements the interface specified in the abstract user role
- Non-share role: An external state that cannot be shared and is injected as a parameter into the methods associated with the specific share
- Privilege factory role: Is responsible for creating and managing privilege roles. When a customer object requests a privilege object, the privilege factory checks whether there is a qualified privilege object in the system. If there is a privilege object, the privilege factory provides it to the customer
Enjoy element mode structure diagram
UnsharedConcreteFlyweight
A non-shared meta-role, which contains non-shared external status information infoFlyweight
Is an abstract meta-role that contains meta-methodsoperation(UnsharedConcreteFlyweight state)
The external state of non-privileged elements is passed in as parametersConcreteFlyweight
Is a concrete user role that contains the keyword key and implements the abstract user interfaceFlyweightFactory
Is the privileges factory role, which is the key to manage specific privileges- The customer role obtains a specific privilege from the privilege factory and accesses methods related to the privilege
13.4.3 Mode Implementation
Abstract meta-roles
public interface Flyweight {
/** * Inject the external state of the non-privileged role */
void operation(UnshareConcreteFlyweight outState);
}
Copy the code
Non-privileged role
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UnshareConcreteFlyweight {
private String info;
}
Copy the code
Specific share meta role
@Slf4j
public class ConcreteFlyweight implements Flyweight{
private String key;
public ConcreteFlyweight(String key) {
this.key = key;
log.info("Concrete share {} was created",key);
}
@Override
public void operation(UnshareConcreteFlyweight outState) {
log.info("Specific element {} is called",key);
log.info("Non-privileged information is :{}",outState.getInfo()); }}Copy the code
Enjoy yuan factory role
@Slf4j
public class FlyweightFactory {
private HashMap<String,Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) {
Flyweight flyweight = flyweights.get(key);
if(flyweight ! =null) {
log.info("Share {} already exists, obtained successfully",key);
} else {
flyweight = new ConcreteFlyweight(key);
flyweights.put(key,flyweight);
}
returnflyweight; }}Copy the code
Client testing
public class FlyweightPattern {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight f01 = factory.getFlyweight("a");
Flyweight f02 = factory.getFlyweight("a");
Flyweight f03 = factory.getFlyweight("a");
Flyweight f11 = factory.getFlyweight("b");
Flyweight f12 = factory.getFlyweight("b");
f01.operation(new UnsharedConcreteFlyweight("First call to A"));
f02.operation(new UnsharedConcreteFlyweight("Call a the second time"));
f03.operation(new UnsharedConcreteFlyweight("Third call to A"));
f11.operation(new UnsharedConcreteFlyweight("The first call to B"));
f12.operation(new UnsharedConcreteFlyweight("Call b the second time")); }}Copy the code
The test results
The memory address is only created when it is first retrieved, and then retrieved if it exists
2.4.4 Integer Wraps the free element schema in the class
First we introduce Integer with a small test:
@Slf4j
public class IntegerTest {
public static void main(String[] args) {
Integer a = Integer.valueOf(127);
Integer b = 127;
Integer c = Integer.valueOf(128);
Integer d = 128;
log.info("{}", a == b);
log.info("{}",c == d); }}Copy the code
Test results:
You can see that the wrapper class created by 127 is an object no matter what, whereas the wrapper class created by 128 is a new object every time, right
Source code analysis
If I is between integerCache. low and integerCache. high, the object is retrieved from the cache array of the IntegerCache class
If not, create a new wrapper object
So what exactly is an IntegerCache?
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if(integerCacheHighPropValue ! =null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache(a) {}}Copy the code
IntegerCache is an inner class of the Integer class
Let’s understand the IntegerCache class comment:
IntegerCache default to the Integer – 128-127 to the cache, but can be modified by the JVM startup the largest cache values, start the instructions for the Java. Lang. Integer. IntegerCache. High
The static code block is then analyzed:
Here to create a variable h is 127, and then get in the JVM startup to Java. Lang. Integer. IntegerCache. The value of the high command
If the value exists, assign h to the maximum value between 127 and the value obtained by the instruction, and then assign H to the variable high (which has already achieved the maximum value of the custom cache).
The cache array is then created by traversing
From the above analysis, we know that when a static property in IntegerCache is called when the Integer#valueOf method is called, IntegerCache must be initialized and a static code block must be called
So why is it that when you create a wrapper object with a value of 127 it’s actually the same object
2.4.5 Customizing connection Pools
Let’s start by customizing a Connection class by implementing the Connection interface
@ToString
class MockConnection implements Connection {
@Override
public Statement createStatement(a) throws SQLException {
return null; }... }Copy the code
Then define a custom connection pool
@Slf4j
public class Pool {
// The connection pool size
private int poolSize;
// Join an array of objects
private Connection[] connections;
// Connection status array, 0 indicates unused, 1 indicates used
private AtomicIntegerArray states;
// constructor
public Pool(int poolSize) {
this.poolSize = poolSize;
connections = new Connection[poolSize];
states = new AtomicIntegerArray(poolSize);
for (int i = 0; i < poolSize; i++) {
connections[i] = newMockConnection(); }}// Get the connection
public Connection getConnection(a) {
while (true) {
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) {
states.compareAndSet(i,0.1);
log.info("get {}",connections[i]);
returnconnections[i]; }}// Block if there are no free connections
synchronized (this) {
try {
log.info("wait...");
this.wait();
} catch(InterruptedException e) { e.printStackTrace(); }}}}// Return the connection
public void free(Connection connection) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == connection) {
states.set(i,0);
log.info("free {}",connections[i]);
break; }}synchronized (this) {
this.notifyAll(); }}}Copy the code
Defining connection pools
Because the array thread is not safe, you need to use the AtomicIntegerArray to ensure that you can modify the values in the array simultaneously without concurrency problems
When the constructor is called to specify the size of the connection pool with the parameter passed in, and the array of connection objects and connection objects is created from the pool size, you can see that each object in the array of connection objects is a MockConnection object
Define fetch connection
The entire connection pool is first traversed, and if there is a thread object with a state of 0, the connection status information is first modified through CAS, and then the connection is acquired
If there are no free connections, the current thread is blocked until another thread releases the connection and wakes up
Define release connection
If so, set the connection status to 1. (Because this connection object is acquired by a thread, other threads cannot obtain this connection, so there is no security problem.)
When the connection is released, the thread currently blocked is woken up
The test class
public class Test {
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection connection = pool.getConnection();
try {
Thread.sleep(new Random().nextInt(1000));
} catch(InterruptedException e) { e.printStackTrace(); } pool.free(connection); }).start(); }}}Copy the code
The test results
You can see that only two threads can acquire connections, and they can only be reacquired when they are released
details
-
In fact, there is still a problem with the code. The connection state array needs to be modified with the volatile keyword to keep it visible (otherwise, if some thread has changed the state but there is no update in main memory, the concurrency error will still occur).
-
The reason for using the while loop when acquiring a connection is that when the thread that released the connection wakes up the blocking thread, it must ensure that the connection pool is checked again for existence (because it may have been acquired by another thread).
The above implementation does not consider:
-
Dynamic growth and contraction of connections
-
Keepalive connection (Availability check)
Cannot detect whether a connection is valid when it fails for some reason (such as heartbeat detection)
-
Wait timeout processing
We use this.wait() to block the thread, but do not deal with the blocking time, in order to prevent loop attempts when a connection is fetched but not yet fetched
-
Distributed hash
My personal official account is under preliminary construction now. If you like, you can follow my official account. Thank you!