Note: it doesn't matter if you don't understand the optimistic lock scheme of [problem proposal], which is what this paper will discuss
Article catalog
Ask questions
There is an account with two functions: withdrawal and balance query. How to ensure that multiple threads withdraw money at the same time without concurrent problems?
Account interface:
interface DecimalAccount{ //Get balance BigDecimal getBalance() ; //withdraw money void withdraw(BigDecimal account) ; //Simulate multi thread withdrawal static void demo(DecimalAccount account){ List<Thread> ts = new ArrayList<>() ; /** * Method will start 1000 threads, and each thread will do - 10 yuan operation * If the initial balance is 10000 then the correct result should be 0 */ for (int i = 0; i < 1000; i++) { ts.add(new Thread(()->{ account.withdraw(BigDecimal.TEN); })); } ts.forEach(Thread::start); ts.forEach(t->{ try { t.join(); //Synchronize the following print statements } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(account.getBalance()); } }
Thread unsafe implementation:
public class TestAtomicRef implements DecimalAccount { BigDecimal balance ; public TestAtomicRef(BigDecimal balance) { this.balance = balance; } @Override public BigDecimal getBalance() { return balance ; } /*Unsafe implementation*/ @Override public void withdraw(BigDecimal amount) { BigDecimal balance = this.getBalance() ; this.balance = balance.subtract(amount) ; } } //Call function class Test{ public static void main(String[] args) { TestAtomicRef ref = new TestAtomicRef(new BigDecimal(10000)) ; DecimalAccount.demo(ref); } }
The expected result is 0, and the rate of result generation is not 0, so the thread is unsafe
Solution 1: lock
//Weight lock private final Object lock = new Object(); /*Security implementation - heavyweight lock*/ @Override public void withdraw(BigDecimal amount) { synchronized (lock){ BigDecimal balance = this.getBalance() ; this.balance = balance.subtract(amount) ; }
Ensure full synchronization of withdrawal operations
Solution 2: CAS operation (no lock / optimistic lock)
//CAS AtomicReference<BigDecimal> ref ; public TestAtomicRef(BigDecimal balance) { ref = new AtomicReference<>(balance) ; } @Override public BigDecimal getBalance() { return ref.get() ; } /*Security implementation CAS*/ @Override public void withdraw(BigDecimal amount) { while (true){ BigDecimal prev = ref.get() ; BigDecimal next = prev.subtract(amount) ; if (ref.compareAndSet(prev,next)) break; } }
CAS analysis
The solution of AtomicInteger that we saw earlier is that there is no lock inside to protect the thread safety of shared variables. So how does it work?
Mainly this sentence
if (ref.compareAndSet(prev,next)) break;
- compareAndSet does this check. Before setting, compare the latest value of prev and ref in memory
- If it is inconsistent, next is voided, and false is returned to indicate failure. For example, if other threads have made subtraction and the current value has been reduced to 990, then this 990 (next) of this thread will be invalid, and enter the while next loop to try again
- If it is consistent, set next as the new value and return true to indicate success
compareAndSet, whose abbreviation is CAS (also known as Compare And Swap), must be atomic operation
be careful:
The bottom layer of CAS is lock cmpxchg instruction (X86 architecture), which can guarantee the atomicity of [compare exchange] under single core CPU and multi-core CPU
- lock: a variable that acts on main memory and marks a variable as a thread exclusive state
In the multi-core state, when a certain core executes the instruction with lock, the CPU will lock the bus. When the core finishes executing the instruction, it will turn on the bus. This process will not be interrupted by the thread scheduling mechanism, which ensures the accuracy of memory operation of multiple threads, and is atomic.
Group counting bus sniffing mechanism
CAS must use volatile to read the latest value of shared variables to achieve the effect of [compare and exchange]
– please refer to:
Valatile principle - memory barrier
Why the efficiency of no lock (CAS) is high
- When there is no lock, even if the retry fails, the thread is always running at a high speed and there is no pause (while), while synchronized will cause context switching and blocking when the thread does not get the lock.
Make a comparison
-
The thread is like a racing car on a high-speed track. When running at high speed, the speed is super fast. Once the context switch occurs, it is like a racing car to slow down, turn off, etc. when awakened, it has to restart, start and accelerate It costs a lot to recover to high speed operation
-
But in the case of no lock, because the thread needs the support of extra CPU to keep running, the CPU here is just like a high-speed runway. There is no extra runway, and the thread can't talk about running at high speed. Although it won't enter the block, it will still enter the runnable state because it is not divided into time slices, which will still lead to context switching.
CAS features
Combining CAS and volatile can realize lockless concurrency, which is suitable for scenarios with few threads and multi-core CPU.
-
CAS is based on the idea of optimistic lock: the most optimistic estimation, not afraid of other threads to modify shared variables, even if it is changed, it doesn't matter. I will try again if I suffer losses.
-
synchronized is based on the idea of pessimistic lock: the most pessimistic estimate is to prevent other threads from modifying shared variables. If I lock, you don't want to change it. Only when I know how to unlock, can you have a chance.
-
CAS embodies the non lock concurrency and non blocking concurrency. Please carefully understand the meaning of these two sentences
-
Because synchronized is not used, threads will not get stuck, which is one of the factors for efficiency improvement
-
But if the competition is fierce, we can think that retries will happen frequently, but the efficiency will be affected
JUC_Atomic class
ABA problem
The so-called ABA means that thread 1 should change "A" to "C", assuming that thread 2 Changes "A" to "B" and "A" before thread 1 finishes executing. At this time, thread 1 cannot know that the shared variable "A" has been modified, and CAS execution will still succeed. Please see the following simulation code
package com.Thread; import java.util.concurrent.atomic.AtomicReference; public class TestABA { static AtomicReference<String> ref = new AtomicReference<>("A"); public static void main(String[] args) throws InterruptedException { System.out.println("Main thread start"); String prev = ref.get() ; AtoBtoA(); Thread.sleep(1000); //change ->C boolean c = ref.compareAndSet(prev, "C"); System.out.println("change A->C"+c); } private static void AtoBtoA() throws InterruptedException { new Thread(()->{ String prev = ref.get(); boolean b = ref.compareAndSet(prev, "B"); System.out.println("change A->B"+b); }).start(); Thread.sleep(500); new Thread(()->{ String prev = ref.get(); boolean a = ref.compareAndSet(prev, "A"); System.out.println("change B->A"+a); }).start(); } }
result
Main thread start
change A->Btrue
change B->Atrue
change A->Ctrue
ABA solution AtomicStampedReference
The main thread can only judge whether the value of the shared variable is the same as the original value a, and can't perceive the situation that the value changes from a to B and back to A. if the main thread
Hope:
As long as there are other threads [moved] to share variables, then their cas will fail. At this time, it is not enough to only compare the values, and a version number needs to be added
AtomicStampedReference
package com.Thread; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class SolveABA { static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0); public static void main(String[] args) throws InterruptedException { System.out.println("Main thread start"); String prev = ref.getReference() ; //Get version number int stamp = ref.getStamp(); System.out.println("The main version number is"+stamp); AtoBtoA(); Thread.sleep(1000); boolean c = ref.compareAndSet(prev, "C", stamp, stamp + 1); //change ->C System.out.println("change A->C"+c); } private static void AtoBtoA() throws InterruptedException { new Thread(()->{ String prev = ref.getReference(); boolean b = ref.compareAndSet(prev, "B", ref.getStamp(), ref.getStamp() + 1); System.out.println("change A->B"+b); }).start(); Thread.sleep(500); new Thread(()->{ String prev = ref.getReference(); boolean a = ref.compareAndSet(prev, "A", ref.getStamp(), ref.getStamp() + 1); System.out.println("change B->A"+a); }).start(); } }
result:
Major version number is 0
change A->Btrue
change B->Atrue
change A->Cfalse
Summary:
Atomicstampededreference can add version number to the atomic reference and track the whole change process of the atomic reference, such as a - > b - > A - > C. through atomicstampededreference, we can know that the reference variable has been changed several times in the middle
Atomic array
Atomic integers are only useful for shared variables of a single value, but they do not guarantee the thread safety of elements in a collection or array. You can use AtomicIntegerArray to ensure the thread safety of elements in a collection or array
The following code can test whether the array is thread safe. This method will start multiple threads to automatically increase the elements in the array
If the initial value of ten elements in the array is 0, ten threads will automatically increase ten elements in the array by 10000 times, and the thread safety result should be
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
package com.Thread; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; public class TestAtomicArray { /*The general method of testing array security is to provide parameters by functional programming*/ /** Parameter 1, provide array, can be thread unsafe array or thread safe array Parameter 2, the method to get the array length Parameter 3, auto increment method, return array, index Parameter 4, print array method */ private static <T> void demo( Supplier<T> arraySupplier, Function<T, Integer> lengthFun, BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer) { List<Thread> ts = new ArrayList<>(); T array = arraySupplier.get() ; Integer length = lengthFun.apply(array); for (int i = 0; i < length ; i++) { //Each thread performs 1000 operations on the array ts.add(new Thread(()->{ for (int j = 0; j < 10000; j++) { putConsumer.accept(array,j%length);//Modulus is to be evenly distributed over each element of the array } })); } ts.forEach(t -> t.start()); // Start all threads ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); // Wait for all threads to finish printConsumer.accept(array); }
Unsafe verification:
public static void main(String[] args) { demo( ()->new int[10], (array)->array.length, (array,index)->array[index]++, //Self increasing array-> System.out.println(Arrays.toString(array)) ); } }
result:
[9224, 9254, 9278, 9262, 9248, 9252, 9278, 9280, 9233, 9293]
Security verification:
//safe demo( ()->new AtomicIntegerArray(10), (array)->array.length(), (array,index)->array.getAndIncrement(index), array-> System.out.println(array) );
result:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
Field updater atomicintegerfield Updater
This updater can make safe operation on the properties of the class
package com.Thread; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; public class TestFieldUpdater { private volatile int field ; public static void main(String[] args) { AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(TestFieldUpdater.class,"field") ; TestFieldUpdater updater = new TestFieldUpdater(); fieldUpdater.compareAndSet(updater,0,10); // Modified successfully field = 10 System.out.println(updater.field); // Modified successfully field = 20 fieldUpdater.compareAndSet(updater,10,20); System.out.println(updater.field); // Modified successfully field = 20 fieldUpdater.compareAndSet(updater,10,30); System.out.println(updater.field); } }
Atomic class common operations
package com.Thread; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicMarkableReference; public class TestAutomic { public static void main(String[] args) { AtomicInteger i = new AtomicInteger(0) ; // Get and auto increment (i = 0, result i = 1, return 0), similar to i++ System.out.println(i.getAndIncrement()); // Auto increment and get (i = 1, result i = 2, return 2), similar to + + i System.out.println(i.incrementAndGet()); /* Get and update (i = 0, p is the current value of i, result i = -2, return 0) The operation in the function can guarantee the atom, but the function needs no side effect*/ System.out.println(i.getAndUpdate(p->p+2)); /* Update and get (i = -2, p is the current value of i, result i = 0, return 0) The operation in the function can guarantee the atom, but the function needs no side effect*/ System.out.println(i.updateAndGet(p -> p + 2)); /*Get and calculate / calculate and get*/ //p = i ; x = 10 ; System.out.println(i.getAndAccumulate(10, (p, x) -> p + x)); //p = i ; x = 10 ; System.out.println(i.accumulateAndGet(10, (p, x) -> p + x)); } }