i++ is not a thread-safe operation because it is not an atomic operation.
So, if I want to achieve this effect similar to i++, which collection or tool class should I use?
Before JDK1.5, in order to ensure the atomicity of a certain 基本
data type or 引用
data type operation under multithreading , you must rely on external keywords synchronized
, but this situation has changed after JDK1.5, of course you can still use synchronized To ensure atomicity, one of the thread-safe methods we are talking about here is atomic tool classes, such as AtomicInteger, AtomicBoolean, etc. These atoms classes are thread-safe tools, they are also Lock-Free
of. Let's take a look at these tools and what the concept of Lock-Free is.
Understanding AtomicInteger
AtomicInteger
It is a newly added tool class in JDK 1.5, let’s first look at its inheritance relationship
Like the wrapper class Integer int, they are inherited from the Number
class.
The Number class is a wrapper class for basic data types. Generally, objects related to data types inherit from the Number class.
Its inheritance system is very simple, let’s take a look at its basic properties and methods
Basic properties of AtomicInteger
There are three basic attributes of AtomicInteger
Unsafe
A sun.misc
package following class, AtomicInteger mainly dependent on the number of native methods sun.misc.Unsafe provide guarantees atomicity operation.
Unsafe The objectFieldOffset
method may acquire the address in memory member property with respect to the object memory address offset. To put it simply, find the address of this variable in memory, which is convenient for subsequent operations directly through the memory address. This value isvalue
We will elaborate on this later
value
Is the value of AtomicIneger.
The construction method of AtomicInteger
Continuing to look down, there are only two construction methods for AtomicInteger. One is a parameterless construction method. The default value of the parameterless construction method is 0. The parameterized construction method can specify the initial value.
Methods in AtomicInteger
Let's talk about the methods in AtomicInteger.
Get and Set
Let's first look at the simplest get and set methods:
get()
: Get the current value of AtomicInteger
set()
: Set the current value of AtomicInteger
AtomicInteger atomic data can be read in the get (), set () can set the current atomic value because get () and set () are ultimately applied to the variable value, and the value is volatile
modified, so get and set are equivalent to reading and setting the memory. As shown below
We mentioned the non-atomic operations of i++ and i++ above, we said that we can use the method in AtomicInteger to replace.
Incremental operation
AtomicInteger in the Incremental
relevant way to meet our needs
getAndIncrement()
: Atomically increase the current value and return the result. Equivalenti++
operation.
In order to verify whether it is thread-safe, we use the following example to test
public class TAtomicTest implements Runnable{
AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for(int i = 0;i < 10000;i++){
System.out.println(atomicInteger.getAndIncrement());
}
}
public static void main(String[] args) {
TAtomicTest tAtomicTest = new TAtomicTest();
Thread t1 = new Thread(tAtomicTest);
Thread t2 = new Thread(tAtomicTest);
t1.start();
t2.start();
}
}
By outputting the result, you will find that it is a thread-safe operation. You can modify the value of i, but the final result is still i-1, because the value is taken first, and then + 1, its schematic diagram is as follows.
incrementAndGet
In contrast, the + 1 operation is performed first, and then the result of the increment is returned. This operation method can ensure the atomic operation of the value. As shown below
Decremental operation
In contrast, decrement operations such as x-- or x = x-1 are also atomic. We can still use the method in AtomicInteger to replace
getAndDecrement
: Return the int value of the current type, and then decrement the value of value. Below is the test code
class TAtomicTestDecrement implements Runnable{
AtomicInteger atomicInteger = new AtomicInteger(20000);
@Override
public void run() {
for(int i = 0;i < 10000 ;i++){
System.out.println(atomicInteger.getAndDecrement());
}
}
public static void main(String[] args) {
TAtomicTestDecrement tAtomicTest = new TAtomicTestDecrement();
Thread t1 = new Thread(tAtomicTest);
Thread t2 = new Thread(tAtomicTest);
t1.start();
t2.start();
}
}
The following is a schematic diagram of getAndDecrement
decrementAndGet
: Similarly, the decrementAndGet method is to perform the decrement operation first, and then get the value of value, the diagram is as follows
LazySet method
Do you know that volatile has a memory barrier?
What is the memory barrier?
Memory barrier, also known as
内存栅栏
memory barrier, barrier instruction, etc., is a type of synchronization barrier instruction, which is a synchronization point in the operation of random access to memory by the CPU or compiler, so that all read and write operations before this point are executed Only then can you start to perform operations after this point. It is also a technology that makes the memory state in the CPU processing unit visible to other processing units.
The CPU uses a lot of optimizations, such as the use of cache, instruction rearrangement, etc. The ultimate goal is performance, that is, when a program is executed, as long as the final result is the same, it does not matter whether the instructions are rearranged. Therefore, the execution timing of instructions is not executed sequentially, but executed out of order, which will bring many problems, which also promotes the emergence of memory barriers.
Semantically, all write operations before the memory barrier must be written to the memory; read operations after the memory barrier can obtain the result of the write operation before the synchronization barrier. Therefore, for sensitive program blocks, memory barriers can be inserted after write operations and before read operations.
The overhead of the memory barrier is very lightweight, but no matter how small it is, there is an overhead. LazySet does exactly this. It will read and write variables in the form of ordinary variables.
It can also be said: too lazy to set up barriers
GetAndSet method
Atomically set to the given value and return the old value.
Its source code is to call the getAndSetInt method in unsafe, as shown below
Was first circulated, and then call the getIntVolatile
method, which I did not find in cpp, the small partners to find remember to tell me to learn about.
Loop until compareAndSwapInt returns false, which means that CAS is not updated to the new value, so var5 returns the latest memory value.
CAS method
We have often said that the CAS is actually a CompareAndSet
method, which as the name suggests, is to compare and update the meaning of course, this is literally, literally a little deviation, in fact, people mean to compare, if satisfied then no longer be updated.
Given above CAS Java source code level, JDK give it official explanation is that if the current value equal to the value of expect, then the atomic nature of the current value is set to update the given value , this method returns a boolean type, If it is true, it means the comparison and update succeeded, otherwise it means it failed.
CAS is also a lock-free concurrency mechanism, also known as it Lock Free
, so do you think Lock Free is very big? not at all.
Below we build a locked and unlocked CASLock
class CASLock {
AtomicInteger atomicInteger = new AtomicInteger();
Thread currentThread = null;
public void tryLock() throws Exception{
boolean isLock = atomicInteger.compareAndSet(0, 1);
if(!isLock){
throw new Exception("加锁失败");
}
currentThread = Thread.currentThread();
System.out.println(currentThread + " tryLock");
}
public void unlock() {
int lockValue = atomicInteger.get();
if(lockValue == 0){
return;
}
if(currentThread == Thread.currentThread()){
atomicInteger.compareAndSet(1,0);
System.out.println(currentThread + " unlock");
}
}
public static void main(String[] args) {
CASLock casLock = new CASLock();
for(int i = 0;i < 5;i++){
new Thread(() -> {
try {
casLock.tryLock();
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}finally {
casLock.unlock();
}
}).start();
}
}
}
In the above code, we built a CASLock, in the tryLock
method, we first use the CAS method update, the update is not successful if an exception is thrown and the current thread is set to locking thread. In the unLock
method, we first determine whether the current value is 0, 0 if it is the result of what we would like to see a direct return. Otherwise, it means that the current thread is still locked. Let's judge whether the current thread is a locked thread, and if it is, perform the unlock operation.
Then the compareAndSet we mentioned above can actually be parsed as the following operation
// 伪代码
// 当前值
int v = 0;
int a = 0;
int b = 1;
if(compare(0,0) == true){
set(0,1);
}
else{
// 继续向下执行
}
You can also take the example of buying a ticket in a life scene. You must hold a ticket to enter the scenic spot. If you have a fake ticket or a ticket that does not meet the scenic spot, it will definitely be able to be identified. You certainly can't enter the scenic spot.
Stop talking nonsense, here is a schematic diagram of compareAndSet
weakCompareAndSet
: Damn, I watched it several times very carefully, and found that this method of JDK1.8 is exactly the same as the compareAndSet method, cheating me. . .
But is this really the case? No, the JDK source code is very extensive and profound, so you won't design a repetitive method. Think about it, the JDK team will not commit such a low-level team, but what is the reason?
The book "Java High Concurrency Detailed" gives us an answer
AddAndGet
AddAndGet and getAndIncrement, getAndAdd, incrementAndGet and other methods all use the do...while + CAS operation, which is actually equivalent to a spin lock. If the CAS modification is successful, it will keep looping, and the modification failure will return. The schematic is as follows
Dive into AtomicInteger
We discussed the specific use of AtomicInteger above. At the same time, we know that AtomicInteger relies on volatile and CAS to ensure atomicity, so let's analyze why CAS can guarantee atomicity and what is its underlying layer? What does AtomicInteger have to do with optimistic locking?
The underlying implementation principle of AtomicInteger
Let us look at this lovely compareAndSetL(CAS)
way why these two lines of code to ensure the atomicity?
We can see that this method is equivalent to calling the CAS in unsafe compareAndSwapInt
methods, we can only send into unsafe looks into implementation.
compareAndSwapInt is sun.misc
the method, which is a native
method, it is the underlying C / C ++ to achieve, so we need to see the C / C ++ source code.
Do you know the awesomeness of C/C++? Using Java is playing the application and architecture, and C/C++ is playing the server and the bottom layer.
compareAndSwapInt source in jdk8u-dev/hotspot/src/share/vm/prims/unsafe.app
the path, its source is achieved
That is Unsafe_CompareAndSwapInt
the method we have found this method
I don't understand the C/C++ source code, but this does not prevent us from finding the key code Atomic::cmpxchg
. cmpxchg is an assembly instruction of the x86 CPU architecture. Its main function is to compare and exchange operands. Let's continue to find the definition of this instruction.
We will find that corresponding to different os, the underlying implementation is different
We found the implementation of Windows as follows
We continue to look down, it actually defines the code on line 216, we look in
At this time, knowledge of assembly instructions and registers is required.
The above os::is-MP()
is a multiprocessing operating system interface, the following is __asm, it is a C / C ++ keywords used to call the inline assembler.
__asm assembler code is, broadly speaking is to dest, exchange_value, compare_value values are placed in the register, following LOCK_IF_MP
the general meaning of the code is
If it is a multi-processor, it will execute lock and then perform comparison operations. Cmp wherein the comparison indicates, is represented by MP MultiProcess
, je
expressed equal jump, the L0 represents the flag.
We return to the assembly instructions above, we can see, CAS is the underlying cmpxchg
instructions.
Optimistic lock
Do you have this question, why AtomicInteger can get the current value, then why is there still expectValue
and value
inconsistency of it?
Because AtomicInteger just one atom of tools, it is not exclusive, it is not like synchronized
or is lock
the same as having mutually exclusive and exclusionary, remember that there are two ways to get AtomicInteger and set it? They just use volatile
modified a bit, but does not have the atomic volatile, so there may be the current value and the value of expectValue inconsistent, so there may be repeated modifications.
There are solutions to this situation for the above two, one is using synchronized
and lock
and similar locking mechanism, which has the exclusive lock, which means that only one thread at a time to be modified in this way can guarantee Atomicity, but relatively expensive, this kind of lock is also called pessimistic lock. Another solution is to use 版本号
or yes CAS 方法
.
version number
The version number is in the data table mechanism adding a version
field to achieve, indicates the number of data is modified, when the write operation is performed and the write was successful, version = version + 1, when the thread A to update the data, read The data will also read the version value at the same time. When submitting the update, if the version value read just now is equal to the version value in the current database, it will be updated. Otherwise, the update operation will be retried until the update is successful.
CAS method
Another way is CAS. We have used a lot of space above to introduce the CAS method. Then we think that you have a certain understanding of its operating mechanism, and we will not elaborate on its operating mechanism.
Everything has advantages and disadvantages. There is no perfect solution in the software industry but the best solution. So Optimistic Lock also has its weaknesses and defects, that is, the ABA problem.
ABA problem
The ABA problem says that if the value of a variable read for the first time is A, and when you are ready to write to A, you find that the value is still A, then in this case, it can be considered that the value of A has not been changed. ? It can be the case of A -> B -> A, but AtomicInteger doesn't think so. It only believes what it sees, and what it sees is what it sees. For example
If there is a singly linked list, as shown in the figure below
A.next = B and B.next = null. At this time, two threads T1 and T2 respectively take out A from the singly linked list. For some special reasons, T2 changes A to B and then to A. At this time, T1 Execute the CAS method and find that the singly-linked list is still A, and the CAS method will be executed. Although the result is correct, this operation will cause some potential problems.
At this time, it is still a singly linked list. Two threads T1 and T2 respectively take out A from the singly linked list, and then T1 changes the linked list to ACD as shown in the figure below
At this time T2, if the memory value is still A, it will try to replace the value of A with B, because the reference of B is null, then C and D will be in a free state
JDK 1.5 after AtomicStampedReference
class provides such a capability, wherein the compareAndSet
method first checks whether the present value is equal to the expected value, the current reference standard is determined and the expected reference indicia and indicium are equal and, if all are equal, setting Atomically Is the given update value.
Okay, the above is the Java code flow. Seeing the native, we know that we are going to cpp again. Kailu
Simple explanation is UnsafeWrapper
that wrapper, a change in name only. Then after some JNI processing, because compareAndSwapOject compares by reference, it needs to go through C++ object-oriented conversion. The main method isatomic_compare_exchange_oop
As you can see, familiar vocabulary has appeared again cmpxchg
, that is to say, compareAndSwapOject still uses cmpxchg atomic instruction, but it has gone through a series of conversions.
postscript
A question is raised, can CAS guarantee visibility between variables? why?
There is another question, where getIntVolatile
is the cpp source code of the method? How to find?
If the top bosses are interested in these two issues, welcome to communicate.
Follow the official account programmer cxuan and reply to cxuan to receive high-quality information.
I have written six PDFs myself, very hardcore, the links are as follows
I have written six PDFs myself, very hardcore, the links are as follows
I have written six PDFs myself, very hardcore, the links are as follows