JUC learning - atomic operation class

Keywords: Java Back-end Multithreading JUC

1, Atomic operation class in JUC

1. Introduction to atomic classes in JUC

  • What is atomic operation?

atomic means atom in Chinese. In chemistry, we know that atoms are the smallest unit of general matter and are inseparable in chemical reactions.

Here, atomic means that an operation is non interruptible. Even when multiple threads execute together, once an operation starts, it will not be disturbed by other threads. Therefore, the so-called atomic class is simply a class with atomic operation characteristics. The atomic operation class provides some methods to modify data. These methods are atomic operations, which can ensure the correctness of the modified data in the case of multiple threads.

JUC provides strong support for atomic operations. These classes are located in the java.util.concurrent.atomic package, as shown in the following figure:

  • Atomic mind map in JUC

2. Basic type atomic class

Update base types atomically

  • AtomicInteger: int type atomic class
  • AtomicLong: long type atomic class
  • AtomicBoolean: a boolean type atomic class

The methods provided by the above three classes are almost the same. Here, take AtomicInteger as an example.

Construction method of AtomicInteger:

public AtomicInteger(int initialValue) {  // Assign initial value
    value = initialValue;
}

public AtomicInteger() {   // The initial value defaults to 0
}

Common methods of AtomicInteger class

public final int get() //Gets the current value
public final int getAndSet(int newValue)//Gets the current value and sets a new value
public final int getAndIncrement()//Gets the current value and increments it automatically
public final int getAndDecrement() //Get the current value and subtract from it
public final int getAndAdd(int delta) //Gets the current value and adds the expected value
boolean compareAndSet(int expect, int update) //If the entered value is equal to the expected value, the value is set atomically as the input value (update)
public final void lazySet(int newValue)//Finally, it is set to newValue. After setting with lazySet, other threads may still be able to read the old value for a short period of time.

Partial source code

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

Description of 2 key fields:

  • Value: using volatile decoration can ensure the visibility of value in multithreading.
  • valueOffset: the offset of the value attribute in the AtomicInteger. Through this offset, you can quickly locate the value field, which is the key to the implementation of AtomicInteger.

getAndIncrement source code:

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

The internal call is the getAndAddInt method in the Unsafe class. Let's take a look at the source code of getAndAddInt:

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

explain:

  • this.getIntVolatile: ensures that the latest value of the variable is obtained from main memory.
  • compareAndSwapInt: CAS operation. The principle of CAS is to compare the expected value with the original value. If it is the same, it will be updated to a new value, which can ensure that only one thread will operate successfully in the case of multiple threads. If it is unsuccessful, false will be returned.
  • There is a do while loop above. After compareAndSwapInt returns false, it will get the value of the variable from the main memory again and continue the CAS operation until it succeeds.
  • getAndAddInt operation is equivalent to thread safe count + + operation, as follows:
    synchronize(lock){
    count++;
    }
  • The count + + operation is actually split into three steps:
  1. Get the value of count and record it as A: A=count
  2. Add the value of A + 1 to get B: B = A+1
  3. Assign B to count: count = B
    In the case of multithreading, there will be thread safety problems, resulting in inaccurate data.
  • The synchronized mode will cause the thread that cannot obtain the lock during the time occupation to be blocked, and the performance is relatively low. CAS performance is much faster than synchronized.

incrementAndGet source code:

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

From the above source code, we can see that the essence of the incrementAndGet method is to call the getAndAddInt method of the unsafe class.

[I hope readers can read the source code more. There are official explanations on the source code, and the explanations are relatively clear.]

  • Example

Use AtomicInteger to realize the website traffic counter function, simulate 100 people visiting the website at the same time, and each person visits the website 10 times. The code is as follows:

public class AtomicIntegerDemo {
    //Number of visits
    static AtomicInteger count = new AtomicInteger();
    
    //Simulate access once
    public static void request() throws InterruptedException {
        //The simulation took 5 milliseconds
        TimeUnit.MILLISECONDS.sleep(5);
        //For count atom + 1
        count.incrementAndGet();
    }
    
    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        
        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }
        
        countDownLatch.await();
        
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",Time consuming:" + (endTime - startTime) + ",count=" + count);
    }
    
}

Run the above code and output the result:

main,Time consuming: 142,count=1000

It can be seen from the output that incrementAndGet can ensure the correctness of data in the case of multithreading.

3. Introduction to array type and atomic class

Updating an element in the array in an atomic way can ensure the thread safety of modifying the data in the array.

  • AtomicIntegerArray: shaping array atomic operation class
  • AtomicLongArray: long integer array atomic operation class
  • AtomicReferenceArray: reference type array atomic operation class

The methods provided by the above three classes are almost the same, so let's take AtomicIntegerArray as an example.

Construction method of AtomicIntegerArray:

public AtomicIntegerArray(int length) {
    array = new int[length];
}

public AtomicIntegerArray(int[] array) {
    // Visibility guaranteed by final field guarantees
    this.array = array.clone();
}

Common methods of AtomicIntegerArray class

public final int get(int i) //Gets the value of the element at index=i
public final int getAndSet(int i, int newValue)//Return the current value at index=i and set it to the new value: newValue
public final int getAndIncrement(int i)//Get the value of the element at the position of index=i, and let the element at that position increase by itself
public final int getAndDecrement(int i) //Get the value of the element at the position of index=i and let the element at that position subtract itself
public final int getAndAdd(int delta) //Get the value of the element at index=i and add the expected value
boolean compareAndSet(int expect, int update) //If the entered value is equal to the expected value, set the element value at index=i to the input value (update) atomically
public final void lazySet(int i, int newValue)//Finally, set the element at index=i to newValue. After setting with lazySet, other threads may still be able to read the old value in a short period of time.
  • Example

Count the page visits of the website. Suppose the website has 10 pages. Now simulate 100 people to visit each page 10 times in parallel, and then output the page visits. It should be 1000 times for each page. The code is as follows:

public class AtomicIntegerArrayDemo {
    
    static AtomicIntegerArray pageRequest = new AtomicIntegerArray(new int[10]);
    
    /**
     * Simulate access once
     *
     * @param page Which page to visit
     * @throws InterruptedException
     */
    public static void request(int page) throws InterruptedException {
        //The simulation took 5 milliseconds
        TimeUnit.MILLISECONDS.sleep(5);
        // pageCountIndex is the subscript of pageCount array, indicating the position in the array corresponding to the page
        int pageCountIndex = page - 1;
        pageRequest.incrementAndGet(pageCountIndex);
    }
    
    public static void main(String[] args) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        int threadSize = 100;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        
        for (int i = 0; i < threadSize; i++) {
            Thread thread = new Thread(() -> {
                try {
                    for (int page = 1; page <= 10; page++) {
                        for (int j = 0; j < 10; j++) {
                            request(page);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }
        
        countDownLatch.await();
        
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",Time consuming:" + (endTime - starTime));
        
        for (int pageIndex = 0; pageIndex < 10; pageIndex++) {
            System.out.println("The first" + (pageIndex + 1) + "Page visits" + pageRequest.get(pageIndex));
        }
    }
    
}

Run the above code and output the result:

main,Time: 663
 The number of visits to the first page is 1000
 The number of visits to the second page is 1000
 The number of visits to the third page is 1000
 The number of visits to the fourth page is 1000
 The number of visits to the fifth page is 1000
 The sixth page is visited 1000 times
 The seventh page is visited 1000 times
 The 8th page is visited 1000 times
 The 9th page is visited 1000 times
 The 10th page is visited 1000 times

explain:

The code places the visits of 10 pages in an array of int type. The size of the array is 10. Then operate each element in the array through AtomicIntegerArray to ensure the atomicity of the operation data. incrementAndGet will be called every time. This method needs to pass in the subscript of the array, and then perform atomic + 1 operation on the specified element.

The output results are all 1000. It can be seen that concurrent modification of elements in the array is thread safe. If the thread is unsafe, some data may be less than 1000.

4. Introduction to reference type atomic class

A basic type atomic class can only update one variable. If you need to update multiple variables, you need to use a reference type atomic class.

  • AtomicReference: reference type atomic class
  • AtomicStampedReference: atomic update field atomic class in reference type
  • AtomicMarkableReference: atomic updates reference types with marker bits

AtomicReference is very similar to AtomicInteger. The difference is that AtomicInteger encapsulates integers, while AtomicReference corresponds to ordinary object references. It can ensure thread safety when you modify object references.

While introducing AtomicReference, let's first understand a deficiency in atomic operation logic.

  • ABA problem

As we said before, the condition for the thread to judge whether the modified object can be written correctly is whether the current value of the object is consistent with the expected value.

This logic is correct in a general sense, but there may be a small exception, that is, when you get the current data and prepare to modify it to the new value, the value of the object is modified twice by other threads, and after these two modifications, the value of the object returns to the old value. In this way, the current thread cannot correctly judge whether the object has been modified, This is the so-called ABA problem, which may cause some problems.

Of course, the ABA problem has been mentioned earlier. [explained in CAS blog]

for instance

In order to retain customers, a cake shop decided to give VIP card customers 20 yuan at a time to stimulate customers' recharge and consumption, but the condition is that each customer can only be given once. Now we use AtomicReference to realize this function, and the code is as follows:

public class AtomicReferenceDemo {
    
    //Original balance of account
    static int accountMoney = 19;
    //Used to perform atomic operations on account balances
    static AtomicReference<Integer> money = new AtomicReference<>(accountMoney);
    
    /**
     * Simulate two threads to update the background database at the same time to recharge the user
     */
    static void recharge() {
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    Integer m = money.get();
                    
                    if (m == accountMoney) {
                        if (money.compareAndSet(m, m + 20)) {
                            System.out.println("Current balance:" + m + ",Less than 20, recharge 20 yuan successfully, balance:" + money.get() + "element");
                        }
                    }
                    
                    //Sleep for 100ms
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    
    /**
     * Simulate user consumption
     */
    static void consume() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Integer m = money.get();
            if (m > 20) {
                if (money.compareAndSet(m, m - 20)) {
                    System.out.println("Current balance:" + m + ",More than 10, successful consumption of 20 yuan, balance:" + money.get() + "element");
                }
            }
            //Sleep for 50ms
            TimeUnit.MILLISECONDS.sleep(50);
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        recharge();
        consume();
    }
    
}

Run the above code and output the result:

Current balance: 19, less than 20, recharge 20 yuan successfully, balance: 39 yuan
 Current balance: 39, greater than 20, successful consumption: 20 yuan, balance: 19 yuan
 Current balance: 19, less than 20, recharge 20 yuan successfully, balance: 39 yuan
 Current balance: 39, greater than 20, successful consumption: 20 yuan, balance: 19 yuan
 Current balance: 19, less than 20, recharge 20 yuan successfully, balance: 39 yuan
 Current balance: 39, greater than 20, successful consumption: 20 yuan, balance: 19 yuan
 Current balance: 19, less than 20, recharge 20 yuan successfully, balance: 39 yuan

It can be seen from the output that this account has been recharged repeatedly. The reason is that the account balance has been modified repeatedly, and the modified value is the same as the original value 19, so the CAS operation cannot correctly judge whether the current data has been modified (whether it has been added 20).
Although the probability of this situation is small, it is still possible. Therefore, when this situation is indeed possible in business, we must take more precautions.

JDK also takes this situation into account for us. Using AtomicStampedReference can solve this problem well.

  • Using AtomicStampedReference to solve ABA problems

The fundamental reason why AtomicReference cannot solve the above problems is that the state information of the object is lost in the process of modification. For example, when recharging 20 yuan, a state needs to be marked at the same time to indicate that the user has been recharged.

Therefore, as long as we can record the state value of the object in the modification process, we can well solve the problem that the thread cannot correctly judge the state of the object because the object is repeatedly modified.

AtomicStampedReference does exactly this. It internally maintains not only the value of the object, but also a timestamp (we call it timestamp here. In fact, it can use any shape to represent the state value). When the value corresponding to atomicstampedrenewal is modified, it must update the timestamp in addition to updating the data itself.

When AtomicStampedReference sets the object value, the object value and timestamp must meet the expected value before writing can succeed. Therefore, even if the object value is read and written repeatedly and written back to the original value, improper writing can be prevented as long as there is a variable in the timestamp.

Several APIs of AtomicStampedReference have added information about timestamps on the basis of AtomicReference.

//Compare settings. The parameters are: expected value, write new value, expected timestamp and new timestamp
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp);
//Get current object reference
public V getReference();
//Get current timestamp
public int getStamp();
//Sets the current object reference and timestamp
public void set(V newReference, int newStamp);

Now let's use AtomicStampedReference to modify the above recharge problem. The code is as follows:

public class AtomicStampedReferenceDemo {
    
    //Original balance of account
    static int accountMoney = 19;
    //Used to perform atomic operations on account balances
    static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(accountMoney, 0);
    
    /**
     * Simulate two threads to update the background database at the same time to recharge the user
     */
    static void recharge() {
        for (int i = 0; i < 2; i++) {
            int stamp = money.getStamp();
            
            new Thread(() -> {
                for (int j = 0; j < 50; j++) {
                    Integer m = money.getReference();
                    
                    if (m == accountMoney) {
                        if (money.compareAndSet(m, m + 20, stamp, stamp + 1)) {
                            System.out.println("Current timestamp:" + money.getStamp() + ",Current balance:" + m + ",Less than 20, recharge 20 yuan successfully, balance:" + money.getReference() + "element");
                        }
                    }
                    
                    //Sleep for 100ms
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            
        }
    }
    
    /**
     * Simulate user consumption
     */
    static void consume() throws InterruptedException {
        for (int i = 0; i < 50; i++) {
            Integer m = money.getReference();
            int stamp = money.getStamp();
            
            if (m > 20) {
                if (money.compareAndSet(m, m - 20, stamp, stamp + 1)) {
                    System.out.println("Current timestamp:" + money.getStamp() + ",Current balance:" + m + ",More than 20, successful consumption of 20 yuan, balance:" + money.getReference() + "element");
                }
            }
            
            //Sleep for 50ms
            TimeUnit.MILLISECONDS.sleep(50);
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        recharge();
        consume();
    }
    
}

Run the above code and output the result:

Current timestamp: 1,Current balance: 19, less than 20, recharge 20 yuan successfully, balance: 39 yuan
 Current timestamp: 2,Current balance: 39, greater than 20, successful consumption: 20 yuan, balance: 19 yuan

The result is normal.

This timestamp is also used in the database modification data. For example, two editors edit an article at the same time and submit it at the same time. Only one user is allowed to submit successfully, prompting another user: the blog has been modified by others. How to implement it?

Blog table: t_blog (id,content,stamp). The default value of stamp is 0, and + 1 is updated each time
A. B the two editors edit an article at the same time, and the stamp is 0. When you click Submit, the stamp and id are used as conditions to update the blog content. The sql executed is as follows:

  • update t_blog set content = updated content, stamp = stamp + 1, where id = blog id and stamp = 0;

This update will return the number of affected rows. Only one will return 1, indicating that the update is successful, and the other submitter will return 0, indicating that the data to be modified does not meet the conditions and has been modified by other users.
This way of modifying data is also called optimistic lock.

5. Object attribute modification atomic class introduction

If you need to update a field in a class, you need to use the properties of the object to modify the atomic class.

  • AtomicIntegerFieldUpdater: updates the value of the shaping field
  • AtomicLongFieldUpdater: updates the value of the long field
  • AtomicReferenceFieldUpdater: the value of the atomic update application type field

To update the properties of an object atomically, you need two steps:

  1. In the first step, because the attribute modification type atomic classes of the object are abstract classes, you must use the static method newUpdater() to create an updater every time you use it, and you need to set the classes and attributes you want to update.
  2. Second, the updated object properties must use the volatile modifier.

The methods provided by the above three classes are almost the same, so let's take AtomicReferenceFieldUpdater as an example.

Call AtomicReferenceFieldUpdater static method newUpdater to create AtomicReferenceFieldUpdater object

public static <U, W> AtomicReferenceFieldUpdater<U, W> newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)

explain:

Three parameters

  • tclass: the class of the field to be operated
  • vclass: type of operation field
  • fieldName: field name

Example

Multithreading calls the initialization method of a class concurrently. If it has not been initialized, it will perform initialization. It is required that it can only be initialized once

The code is as follows:

public class AtomicReferenceFieldUpdaterDemo {
    static AtomicReferenceFieldUpdaterDemo demo = new AtomicReferenceFieldUpdaterDemo();
    
    //isInit is used to indicate whether it has been initialized
    volatile Boolean isInit = Boolean.FALSE;
    AtomicReferenceFieldUpdater<AtomicReferenceFieldUpdaterDemo, Boolean> updater = AtomicReferenceFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterDemo.class, Boolean.class, "isInit");
    
    /**
     * Simulate initialization
     *
     * @throws InterruptedException
     */
    public void init() throws InterruptedException {
        //Initialize only when isInit is false, and set isInit to true by atomic operation
        if (updater.compareAndSet(demo, Boolean.FALSE, Boolean.TRUE)) {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",Start initialization!");
            
            //Simulate sleep for 3 seconds
            TimeUnit.SECONDS.sleep(3);
            
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",Initialization complete!");
            
        } else {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",Another thread has already performed initialization!");
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    demo.init();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    
}

Run the above code and output the result:

1638003921152,Thread-0,Start initialization!
1638003921152,Thread-1,Another thread has already performed initialization!
1638003921153,Thread-2,Another thread has already performed initialization!
1638003921153,Thread-3,Another thread has already performed initialization!
1638003921153,Thread-4,Another thread has already performed initialization!
1638003924161,Thread-0,Initialization complete!

explain:

  1. The isInit attribute must be decorated with volatile to ensure the visibility of variables
  2. It can be seen that multiple threads execute the init() method at the same time. Only one thread performs the initialization operation, and other threads skip it. Multiple threads arrive at updater.compareAndSet at the same time, and only one will succeed.

Article reference: http://www.itsoku.com/ Bloggers think the content of this article is very good. If you are interested, you can learn about it.

Posted by damianjames on Wed, 01 Dec 2021 07:29:34 -0800