Optimistic lock of Java multithreading sharing model (CAS and Atomic classes)

Optimistic lock of Java multithreading sharing model (CAS and Atomic classes)

Note: it doesn't matter if you don't understand the optimistic lock scheme of [problem proposal], which is what this paper will discuss

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:

Summary of volatile keywords

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));
    }
}

Tags: Java Programming

Posted on Fri, 19 Jun 2020 05:40:06 -0400 by blmg911