Multithreading from entry to advanced (10) --- atomic classes and CAS

One, atomic operation class

Java has provided the java.util.concurrent.atomic package (hereinafter referred to as the Atomic package) since JDK 1.5. The atomic operation classes in this package provide a simple, efficient, and thread-safe way to update a variable. Because there are many types of variables, there are four types of atomic update methods: atomic update basic type, atomic update array, atomic update reference, and atomic update attribute (field).

1.1 Basic types of atomic updates

  • AtomicBoolean: Atomic update Boolean type.
  • AtomicInteger: Atomic update integer.
  • AtomicLong: Atomic update long integer.
method effect
int addAndGet(int delta) Atomically add the entered value to the value in the instance (value in AtomicInteger) and return the result.
boolean compareAndSet(int expect,int update) If the entered value is equal to the expected value, the value is atomically set to the entered value.
int getAndIncrement () Add 1 to the current value atomically. Note that the value returned here is the value before the increment
int getAndSet(int newValue) Atomically set to the value of newValue and return the old value.

1.2 Atomically update the array

To update an element in the array atomically, the Atomic package provides the following four classes.

  • AtomicIntegerArray: Atomic update of the elements in the integer array.
  • AtomicLongArray: Atomic updates the elements in the long integer array.
  • AtomicReferenceArray: Atomic update of the elements in the reference type array
method effect
int addAndGet(int i,int delta) Atomically add the input value to the element at index i in the array
boolean compareAndSet(int i,int expect,int update) If the current value is equal to the expected value,
the element at position i of the array is atomically set to the update value

1.3 Atomic update quote

AtomicInteger of the basic type can only update one variable. If you want to update multiple variables atomically, you need to use this atom to update the class provided by the reference type. The Atomic package provides the following three categories.

  • AtomicReference: Atomic update reference type.
  • AtomicReferenceFieldUpdater: Atomic update the fields in the reference type.
  • AtomicMarkableReference: Atomic update reference type with mark bits. A Boolean flag and reference type can be updated atomically. The construction method is AtomicMarkableReference (V initialRef, boolean initialMark)

1.4 Atomic update fields

If you need to update a field in a class atomically, you need to use the atomic update field class. The Atomic package provides the following three classes for atomic field update.

  • AtomicIntegerFieldUpdater: An updater that atomically updates integer fields.
  • AtomicLongFieldUpdater: An updater that atomically updates long integer fields.
  • AtomicStampedReference: Atomic update reference type with version number. This class associates integer values ​​with references, which can be used for atomic update data and data version numbers, and can solve the ABA problem that may occur when using CAS for atomic updates.

2. CAS

2.1 Overview

CAS (Compare-And-Swap) looks at the meaning of words, compares and replaces them. Comparison and replacement is to compare an expected value with the current value of a variable. If the value of the current variable is equal to the value we expect, replace the value of the current variable with a new value. The java.util.concurrent.atomic package all uses the idea of ​​CAS.

It is the concurrency primitive of the CPU. Primitives belong to the category of operating systems and are composed of a number of instructions to complete a process of a certain function. The execution of primitives must be continuous and must not be interrupted during execution. In other words, CAS will not cause data inconsistency, CAS is thread-safe

CAS has 3 operands, the memory value V, the old expected value A, and the updated value B to be modified. If and only if the expected value is the same as the memory value V, modify the memory value to B, otherwise do nothing.

2.2 Use of CAS

AtomicInteger atomicInteger = new AtomicInteger(10);
System.out.println(atomicInteger.compareAndSet(10, 2020) + "\t当前的值" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(10, 2019) + "\t当前的值" + atomicInteger.get());

image-20201218112322010

The role of CAS is to compare the value in the current working memory with the value in the main physical memory, if they are the same, update to the value we specified, otherwise continue to compare until the value of the working memory and the main physical memory are the same. We usually call it: compare and exchange

There are two parameters in CAS, the first is the expected value, and the second is the changed value.

2.3 The underlying principle of CAS

There is a method in AtomicInteger: getAndIncrement(). This method can guarantee safety without synchronized. The solution that volatile does not guarantee atomicity has already been mentioned. Then use this method to look at the underlying principle of CAS

public final int getAndIncrement() {
    
    
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
    
    
    int var5;
    do {
    
    
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

CAS的底层使用了unsafe类,由于java需要通过本地(Native)方法才能访问底层系统,unsafe就是这个后门,通过unsafe可以直接操作特定的内存数据。看源码也可以知道unsafe有一堆的本地方法。

The unsafe getAndAddInt method needs to pass in three parameters:

  • var1: current object
  • var2(valueOffset): represents the offset address of the variable in memory
  • var4: the value to be modified

There is a loop judgment method compareAndSwapInt in the getAndAddInt method, which is a local method. Pass in 4 parameters

  • var1: The AtomicInteger object itself
  • var2: The reference address of the object value, which is the offset
  • var4: the value to be modified
  • var5: Compare the value of the current object with var5. If they are the same, update to var5+var4 and return true. If they are different, continue to compare values ​​until the update is complete.

The following figure shows why AtomicInteger can achieve thread safety without synchronized

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

  1. The original value of value in AtomicInteger is 0, that is, the value of AtomicInteger in main memory is 0. According to the JMM model, A and B threads each hold a copy of value=0
  2. Thread A gets the value++ of the CPU resource, and then gets the value var5=0 in the main memory through the getIntVolatile method, performs compareAndSwapInt comparison and exchange, and thread A successfully modifies the Value value in the main memory
  3. Thread B also gets value++ and obtains the value of value var5=1 in the main memory through the getIntVolatile method, compares and exchanges the line compareAndSwapInt and finds that it is not the same, then returns false, enters the next loop, knowing that it finds the value it took and the main memory When the values ​​in are the same, it is worth updating, so as to avoid the overwriting problem of writing, that is, to keep the atomic operation

2.4 Disadvantages of CAS

2.4.1 Long-term spinning consumes resources

Spin is an operation cycle of cas. If a thread is particularly unlucky, every time the value obtained is modified by other threads, then it will continue to spin comparison until it succeeds. In this process, the CPU overhead is very high. It’s big, so try to avoid it.

2.4.2 Can only guarantee the atomic operation of a shared variable
2.4.3 ABA problem

The important premise of the CAS algorithm is that the data at a certain moment in the memory needs to be taken out and at the current moment.Compare and replace, Then this time difference will cause data changes, for example: thread 1 fetches A from the main memory, then thread 2 also fetches A from the main memory, thread 2 changes the value to B after some operations, and then 2. Thread No. changes the value to A again. At this time, when thread No. 1 performs the CAS operation, it is found that the memory is still A, and then the operation of No. 1 thread is successful.

That is to say, thread 2 is invisible to thread 1. There is no problem with the data at this time, but there are some problems in the middle process.

2.5 Use AtomicStampedReference to solve the ABA problem

The ABA problem is that if a thread gets a data that is A at the beginning, and the last modified value is also A, the middle process is not known, and then another thread is not visible to the middle process, and the operation is successful.

To solve the ABA problem, you can add a version number, which means that each thread will change the version number every time it is modified, and then you need to compare the version numbers when comparing, then the ABA problem is solved

The AtomicStampedReference class is provided in the atomic operation class to solve the ABA problem. Look at the following code:

First simulate the generation of the ABA problem:

private static AtomicReference<Character> atomicReference = new AtomicReference<>('A');
public static void main(String[] args) {
    
    
    new Thread(() -> {
    
    
        atomicReference.compareAndSet('A', 'B');
        atomicReference.compareAndSet('B','A');
    },"t1").start();

    new Thread(() -> {
    
    
        try {
    
     TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
    
    e.printStackTrace();}
        System.out.println(atomicReference.compareAndSet('A', 'C') + "\t" + atomicReference.get());
    },"t2").start();
}

image-20201219095136231

The t2 thread is successfully modified to change the value of A to C. Let’s look at using the AtomicStampedReference operation class to solve the ABA problem

private static AtomicStampedReference<Character> atomicStampedReference = new AtomicStampedReference<>('A', 1);

public static void main(String[] args) {
    
    

    new Thread(() -> {
    
    
        int stamp = atomicStampedReference.getStamp();
        System.out.println(Thread.currentThread().getName() + "第1次版本号" + stamp);
        //睡眠1秒保证线程t1拿到当前的版本号
        try {
    
     TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
    
    e.printStackTrace();}
        atomicStampedReference.compareAndSet('A', 'B', atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        System.out.println(Thread.currentThread().getName() + "第2次版本号" + atomicStampedReference.getStamp());
        atomicStampedReference.compareAndSet('B', 'A', atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        System.out.println(Thread.currentThread().getName() + "第3次版本号" + atomicStampedReference.getStamp());
    },"t3").start();


    new Thread(() -> {
    
    
        int stamp = atomicStampedReference.getStamp();
        System.out.println(Thread.currentThread().getName() + "第1次版本号" + stamp);
        //睡眠3秒保证线程t3执行完毕
        try {
    
     TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {
    
    e.printStackTrace();}
        boolean res = atomicStampedReference.compareAndSet('A', 'C', stamp, stamp + 1);
        System.out.println(Thread.currentThread().getName() + "\t修改成功与否" + res);
        System.out.println(Thread.currentThread().getName() + "\t当前实际版本号" + atomicStampedReference.getStamp());
        System.out.println(Thread.currentThread().getName() + "\t当前实际最新值" + atomicStampedReference.getReference());

    },"t4").start();
}

Results of the:

image-20201219100332061

Guess you like

Origin blog.csdn.net/weixin_44706647/article/details/114945728