Introduction to the implementation principle and application of the atomic package in Java, javaatomic
1. Proposal of synchronization problems
Suppose we use A dual-core processor to execute two threads A and B, one is to execute thread A, and the other is to execute thread B, both threads now need to add 1 to the member variable I of the object named obj. Assuming that the initial value of I is 0, theoretically the value of I should be 2 after the two threads run, but in fact, it is very likely that the result is 1.
Here we will analyze the cause. For the sake of simplicity, we will not consider the cache. In fact, there is a possibility that cache will increase the result to 1. Thread A reads the variable I in the memory to the core 1 arithmetic unit, adds 1, and then writes the calculation result back to the memory, because the above operation is not an atomic operation, as long as thread B writes the value of I increase by 1 back to the memory, it reads the value of I in the memory (at this time, the value of I is 0 ), then the result of I is 1. Because the I values read by line A and line B are both 0, the values after the two threads Add 1 to it are all 1, and the two threads successively write 1 to variable I, that is to say, the value of I written twice is 1.
The most common solution is to use the synchronize keyword to lock the obj object for the code with I plus 1 in two threads. Today we will introduce a new solution, that is, using the classes in the Atomic package to solve the problem.
2. Atomic hardware support
In a single processor system (UniProcessor), operations that can be completed in a single command can be considered as "Atomic operations ", because interruptions can only occur between commands (because thread scheduling needs to be completed through interruptions ). This is also why test_and_set, test_and_clear and other commands are introduced in some CPU command systems for the critical resource mutex. The structure of symmetric multi-Processor is different. Because multiple processors in the system run independently, operations that can be completed in a single command may be affected.
On the x86 platform, the CPU provides a means to lock the bus during command execution. There is a lead on the CPU chip # HLOCKpin. If the assembly language program adds the prefix "LOCK" before a command ", the compiled machine code lowers the potential of # HLOCKpin when the CPU executes this command until the end of this command, thereby locking the bus, in this way, other CPUs on the same bus cannot access the memory temporarily through the bus, ensuring the atomicity of this command in a multi-processor environment. Of course, NOT all commands can be prefixed with lock. Only ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NOT, OR, you can add the "LOCK" command before SBB, SUB, XOR, XADD, and XCHG commands to perform atomic operations.
The core operation of Atomic is CAS (compareandset, implemented using the CMPXCHG command, which is an Atomic command). This command has three operands, the memory value of the variable V (the abbreviation of value ), the current expected value of the variable, E (exception), and U (the abbreviation of update). When the memory value is the same as the current expected value, overwrite the updated value of the variable to the memory value. Execute the pseudocode as follows.
if(V == E){ V = U return true }else{ return false }
Now we use CAS to solve the above problems. Line B reads the variable I in the memory to a temporary variable (assuming that the read value is 0), and then reads the value of I to the arithmetic operation unit of core1, next, add 1 to compare whether the values in the temporary variables are the same as the current values of I. If the values are the same, use the results in the computing unit (I + 1) (Note that this part is the CAS operation. It is an atomic operation and cannot be interrupted. CAS operations in other threads cannot be executed at the same time ), otherwise, the command execution fails. If the command fails, it indicates that thread A has added the value of I to 1. It can be seen that if the I value read from both threads at the beginning is 0, only one CAS operation in the same thread will be successful, because the CAS operation cannot be executed concurrently. For the CAS operation execution failure thread, as long as the CAS operation is executed cyclically, it will be successful. We can see that there is no thread blocking, which is essentially different from the principle of synchronize.
3. Introduction to the Atomic package and source code analysis
The basic feature of classes in the Atomic package is that, in a multi-threaded environment, when multiple threads operate on a single (including basic and reference types) variable at the same time, it is exclusive, that is, when multiple threads update the value of the variable at the same time, only one thread can succeed, and the unsuccessful thread can continue to try like the spin lock, until the execution is successful.
The core methods in the Atomic series class call several local methods in the unsafe class. The first thing we need to know is the Unsafe class. The full name is sun. misc. unsafe. This class contains a large number of operations on the C code, including many direct memory allocation and atomic operation calls. The reason why it is marked as Unsafe, is to tell you that a large number of method calls in this will have security risks, you need to be careful to use, otherwise it will lead to serious consequences, such as when the memory is allocated through unsafe, if you specify some areas, some pointers similar to C ++ may cross the border to other processes.
Classes in the Atomic package can be divided into four groups based on the data type of the operation.
AtomicBoolean,AtomicInteger,AtomicLong
Basic thread-safe atomic operations
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
A thread-safe atomic operation of the array type. It operates not the entire array, but a single element in the array.
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
Thread-safe operations based on the basic types (long integer, integer, and reference type) in the reflection principle object
AtomicReference,AtomicMarkableReference,AtomicStampedReference
Thread-safe reference type and atomic operations that prevent reference types of ABA Problems
Generally, we use AtomicInteger, AtomicReference, and AtomicStampedReference. Now let's analyze the source code of AtomicInteger in the Atomic package. The source code of other classes is similar in principle.
1. constructor with Parameters
public AtomicInteger(int initialValue) { value = initialValue;}
It can be seen from the constructor that the value is stored in the value of the member variable.
private volatile int value;
The value of the member variable is declared as volatile type, indicating the visibility under multiple threads. Modifications to any thread are immediately seen in other threads.
2. compareAndSet method (the value is passed through internal this and valueOffset)
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}
This method is the core CAS operation.
3. The getAndSet method calls the compareAndSet method.
public final int getAndSet(int newValue) { for (;;) { int current = get(); if (compareAndSet(current, newValue)) return current; }}
If other threads change the value before executing if (compareAndSet (current, newValue), the value must be different from the value of current. compareAndSet fails to be executed, you can only obtain the value again, and then continue the comparison until the comparison is successful.
4. Implementation of I ++
public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; }}
5. ++ I Implementation
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; }}
4. Example of using AtomicInteger
The following program uses AtomicInteger to simulate the ticketing program. In the running result, no two programs sell the same ticket or the negative number of tickets.
package javaleanning;import java.util.concurrent.atomic.AtomicInteger;public class SellTickets {AtomicInteger tickets = new AtomicInteger(100);class Seller implements Runnable{@Override public void run() {while(tickets.get() > 0){int tmp = tickets.get();if(tickets.compareAndSet(tmp, tmp-1)){System.out.println(Thread.currentThread().getName()+" "+tmp);}}}}public static void main(String[] args) {SellTickets st = new SellTickets();new Thread(st.new Seller(), "SellerA").start();new Thread(st.new Seller(), "SellerB").start();}}
5. ABA Problems
The running result of the preceding example is completely correct. This is based on two or more threads that operate data in the same direction, in the preceding example, both threads perform the decreasing operation on tickets. For example, if multiple threads perform object column operations on a shared queue, the correct result can be obtained through the AtomicReference class (this is the case for the queue maintained in AQS ), however, if multiple threads can be used for columns or columns, that is, the Operation direction of data is inconsistent, ABA may occur.
Now let's take a better understanding example to explain the ABA problem. Suppose there are two threads T1 and T2, and these two threads operate on the same stack for both outbound and inbound operations.
We use the tail defined by AtomicReference to save the top position of the stack.
AtomicReference<T> tail;
Assume that the T1 thread is preparing to exit the stack. For the stack exit operation, we only need to update the top position of the stack from sp to newSP through the CAS operation, as shown in 1. However, before thread T1 executes tail. compareAndSet (sp, newSP), the system executes Thread Scheduling and thread T2. T2 performs three operations: A and B, and then. At this time, the system starts scheduling again, and thread T1 continues to execute the stack operation. However, in the view of thread T1, the top element of the stack is still, (that is, T1 still considers B as the next element of A on the top of stack), as shown in actual case 2. T1 will think that the stack has not changed, so tail. compareAndSet (sp, newSP) is successfully executed, and the top pointer of the stack is directed to Node B. In fact, B does not exist in the stack. T1 shows result 3 after A is output from the stack. This is obviously not the correct result.
6. Solutions to ABA Problems
Use AtomicMarkableReference and AtomicStampedReference. Use the above two Atomic classes for operations. When implementing the compareAndSet command, they must compare the current (operated) and expected (operated) timestamp values in addition to the pre-and pre-defined values of the object, the compareAndSet method can be successful only when all are the same. Each time the update is successful, the stamp value changes. The setting of the stamp value is controlled by the programmer.
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)));}
In this case, the compareAndSet method requires four parameters: expectedReference, newReference, expectedStamp, and newStamp. When using this method, we must ensure that the expected timestamp value and the value to be updated cannot be the same, usually newStamp = expectedStamp + 1
Take the above example
Assume that thread T1 points to A before the stack is popped up, and the stamp value is 100.
Thread T2 execution: After A goes out of the stack, sp points to B and the stamp value changes to 101,
After B leaves the stack, sp points to C and the stamp value changes to 102,
After A enters the stack, sp points to A, and the stamp value changes to 103,
Thread T1 continues to execute the compareAndSet statement and finds that although sp still points to A, the pre-period value 100 of the stamp value is different from the current value 103, so compareAndSet fails, you need to obtain the newSP value (newSP points to C at this time), and the pre-timestamp value 103, and then perform the compareAndSet operation again. In this way, A successfully exits the stack and sp points to C.
Note that compareAndSet can only change one value at a time and cannot change both newReference and newStamp. Therefore, during implementation, a Pair class is defined internally to convert newReference and newStamp into one object, the CAS operation is actually performed on the Pair object.
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); }}
For AtomicMarkableReference, the stamp value is a Boolean variable, while the stamp value in AtomicStampedReference is an integer variable.
Summary
The above is all about the implementation principle and application of the atomic package in Java. I hope it will be helpful to you. If you are interested, you can continue to refer to other related topics on this site. If you have any shortcomings, please leave a message.