Java Review - Concurrent Programming_ Analysis of principles of atomic operation

Posted by mahaguru on Mon, 03 Jan 2022 12:44:22 +0100

Article catalog

summary

The JUC package provides a series of atomic operation classes. These classes are implemented using the non blocking algorithm CAS, which greatly improves the performance compared with using locks to implement atomic operations.

Since the principles of atomic operation classes are roughly the same, we take the implementation principle of AtomicLong class as an example and discuss the principles of LongAdder and LongAccumulator classes added in JDK8

Atomic variable operation class

JUC concurrent package contains AtomicInteger, AtomicLong, AtomicBoolean and other atomic operation classes

AtomicLong is an atomic increment or decrement class, which is implemented internally using Unsafe. Let's look at the following code

package java.util.concurrent.atomic;
import java.util.function.LongUnaryOperator;
import java.util.function.LongBinaryOperator;
import sun.misc.Unsafe;

/**
 * @since 1.5
 * @author Doug Lea
 */
public class AtomicLong extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 1927816293512124184L;

    // 1 get Unsafe instance
    private static final Unsafe unsafe = Unsafe.getUnsafe();
	
	// 2. Offset of storage variable
    private static final long valueOffset;

    /**
     * Records whether the underlying JVM supports lockless
     * compareAndSwap for longs. While the Unsafe.compareAndSwapLong
     * method works in either case, some constructions should be
     * handled at Java level to avoid locking user-visible locks.
     */
	// 3. Judge whether the JVM supports CAS of Long type
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();

    /**
     * Returns whether underlying JVM supports lockless CompareAndSet
     * for longs. Called only once and cached in VM_SUPPORTS_LONG_CAS.
     */
    private static native boolean VMSupportsCS8();

    static {
        try {
			// 4 get the offset of value in AtomicLong
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	
	// 5 actual variable value
    private volatile long value;

    /**
     * Creates a new AtomicLong with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicLong(long initialValue) {
        value = initialValue;
    }
	
	.......
	.......
	.......
	.......
 
}
  • The code (1) obtains an instance of the Unsafe class through the Unsafe.getUnsafe() method Why can I pass Unsafe The getunsafe () method obtains an instance of the Unsafe class? In fact, this is because the AtomicLong class is also under the rt.jar package. The AtomicLong class is loaded through the bootstart class loader.
  • The value in code (5) is declared volatile to ensure memory visibility under multithreading. Value is a variable that stores the count.
  • Code (2) (4) gets the offset of the value variable in the AtomicLong class.

Main methods

incrementAndGet ,decrementAndGet ,getAndIncrement,getAndDecrement

[JDK8+]

//(6) Call the Insafe method, set the atomicity value to the original value + 1, and the return value is the incremented value
	public final long incrementAndGet() {
		return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
	}


//(7) Call the unsafe method, the atomicity setting va1ue value is the original value - 1, and the return value is the value after decrement
 public final long decrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
    }
//(8) Call the unsafe method, the atomicity setting va1ue value is the original value + 1, and the return value is the original value
public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }
//(9) Call the unsafe method, the atomicity setting va1ue value is the original value - 1, and the return value is the original value
  public final long getAndDecrement() {
        return unsafe.getAndAddLong(this, valueOffset, -1L);
    }

We can find that these methods operate internally by calling the getAndAddLong method of Unsafe. This function is an atomic operation.

The first parameter is the reference of AtomicLong instance, the second parameter is the offset value of value variable in AtomicLong, and the third parameter is the value of the second variable to be set.

The implementation logic of getAndIncrement method in JDK 7 is

public final long getAndIncrement(){
	while(true){
		long current=get();
		long next= current + 1;
		if (compareAndSet(current, next))
			return current
	}
}

In the above code, each thread first gets the current value of the variable (since value is a volatile variable, the latest value is obtained here), then increases it by 1 in the working memory, and then uses CAS to modify the value of the variable. If the setting fails, the loop continues to try until the setting succeeds.

The logic in JDK 8 is

unsafe.getAndAddLong(this, valueOffset, -1L);
public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

It can be seen that the loop logic in AtomicLong in JDK 7 has been built in by the atomic operation class UNsafe in JDK 8. The reason for the built-in should be that this function can also be used in other places, and the built-in can improve reusability.

boolean compareAndSet(long expect, long update)

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

We can see that unsafe is still called internally Compareandswaplong method. If the value in the atomic variable is equal to expect, update the value with the update value and return true; otherwise, return false.

Small Demo

An example of threads using AtomicLong to count the number of 0

import java.util.concurrent.atomic.AtomicLong;

/**
 * @author Small craftsman
 * @version 1.0
 * @description: TODO
 * @date 2021/11/30 22:52
 * @mark: show me the code , change the world
 */
public class AtomicLongTest {

    //(10) Create Long type atomic counter
    private static AtomicLong atomicLong = new AtomicLong();

    //(11) Create data source
    private static Integer[] arrayOne = new Integer[]{0, 1, 2, 3, 0, 5, 6, 0, 56, 0};

    private static Integer[] arrayTwo = new Integer[]{10, 1, 2, 3, 0, 5, 6, 0, 56, 0};

    public static void main(String[] args) throws InterruptedException {
        //(12) Thread one counts the number of 0 in array arrayOne
        Thread threadOne = new Thread(() -> {
            int size = arrayOne.length;
            for (int i = 0; i < size; ++i) {
                if (arrayOne[i].intValue() == 0) {
                    atomicLong.incrementAndGet();
                }
            }

        });
        //(13) Thread two counts the number of 0 in array arrayTwo
        Thread threadTwo = new Thread(() -> {
            int size = arrayTwo.length;
            for (int i = 0; i < size; ++i) {
                if (arrayTwo[i].intValue() == 0) {
                    atomicLong.incrementAndGet();
                }
            }
        });
        //(14) Start child thread
        threadOne.start();
        threadTwo.start();
        //(15) Wait for the thread to finish executing
        threadOne.join();
        threadTwo.join();
        System.out.println("count 0:" + atomicLong.get());
    }

}

The two threads count the number of zeros in their own data. Every time they find a 0, they will call AtomicLong's atomic increment method

Summary

In the absence of atomic classes, certain synchronization measures need to be used to implement the counter, such as using the synchronized keyword, but these are blocking algorithms, which have a certain loss of performance. These atomic operation classes introduced here use CAS non blocking algorithm, which has better performance.

However, AtomicLong also has performance problems in the case of high concurrency. JDK 8 provides a LongAdder class with better performance at high and low cost. Listen to the next chapter