[concurrent programming] atomic operations in Java

Posted by five on Fri, 22 Nov 2019 11:48:39 +0100

What is atomic operation

Atomic operation refers to one or more indivisible operations. The execution order of these operations can not be disrupted, and these steps can not be cut and only part of them can be executed (non interruptibility). For example:

//It's an atomic operation
int i = 1;

//Non atomic operation, i + + is a multi-step operation, and can be interrupted.
//i + + can be divided into three steps, the first step is to read the value of i, the second step is to calculate i+1; the third step is to assign the final value to i
i++;

Atomic operations in Java

In Java, we can implement atomic operation through synchronous lock or CAS operation.

CAS operation

CAS is the abbreviation of Compare and swap. This operation is A hardware level operation, which ensures the atomicity of the operation at the hardware level. CAS has three operands, memory value V, old expected value A, and new value B to be modified. If and only if the expected value A is the same as the memory value V, change the memory value V to B, otherwise do nothing. The sun.misc.Unsafe class in Java provides several methods to implement CAS, such as compareAndSwapInt and compareAndSwapLong.

In addition, many atomic operation classes based on CAS are provided under the atomic package of jdk, as shown in the following figure:

Let's use AtomicInteger to see how to use these atomic operation classes.

package com.csx.demo.spring.boot.concurrent.atomic;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {

    private static int THREAD_COUNT = 100;

    public static void main(String[] args) throws InterruptedException {


        NormalCounter normalCounter = new NormalCounter("normalCounter",0);
        SafeCounter safeCounter = new SafeCounter("safeCounter",0);
        List<Thread> threadList = new ArrayList<>();

        for (int i = 0; i < THREAD_COUNT ; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        normalCounter.add(1);
                        safeCounter.add(1);
                    }
                }
            });
            threadList.add(thread);
        }

        for (Thread thread : threadList) {
            thread.start();
        }
        for (Thread thread : threadList) {
            thread.join();
        }
        System.out.println("normalCounter:"+normalCounter.getCount());
        System.out.println("safeCounter:"+safeCounter.getCount());
    }


    public static class NormalCounter{
        private String name;
        private Integer count;

        public NormalCounter(String name, Integer count) {
            this.name = name;
            this.count = count;
        }

        public void add(int delta){
            this.count = count+delta;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getCount() {
            return count;
        }

        public void setCount(Integer count) {
            this.count = count;
        }
    }

    public static class SafeCounter{
        private String name;
        private AtomicInteger count;

        public SafeCounter(String name, Integer count) {
            this.name = name;
            this.count = new AtomicInteger(count);
        }

        public void add(int delta){
            count.addAndGet(delta);
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getCount() {
            return count.get();
        }

        public void setCount(Integer count) {
            this.count.set(count);
        }
    }

}

In the above code, we create a normal counter and an atomic operation counter (counting with atomicinter). Then 100 threads are created and each thread counts 10000 times. In theory, after thread execution, the counter value is 1000000, but the result is as follows:

normalCounter:496527
safeCounter:1000000

Each time it is executed, the value of the normal counter is different, while the counter using atomicinter is 1000000.

Problems in CAS operation

  • ABA problem: CAS needs to check whether the lower value has changed during the operation. If it has not, it will be updated. However, if a value turns from a to B and then to a, it will be found that its value has not changed when CAS is used for inspection, but actually it has changed. The solution to ABA problem is to use version number. Add the version number in front of the variable. Add one to the version number each time the variable is updated, and A-B-A will become 1A-2B-3A.

Starting from Java 1.5, the atomic package of JDK provides an atomicstampededreference class to solve the ABA problem. The compareAndSet method of this class first checks whether the current reference is equal to the expected reference and whether the current flag is equal to the expected flag. If all are equal, set the reference and the value of the flag to the given update value atomically.

  • Long cycle time and high overhead: if spin CAS is not successful for a long time, it will bring huge execution overhead to CPU. If the JVM can support the pause instruction provided by the processor, the efficiency will be improved to some extent. The pause instruction has two functions. First, it can delay the de pipeline execution instruction, so that the CPU will not consume too much execution resources. The delay time depends on the specific implementation version, and on some processors, the delay time is zero. Second, it can avoid the CPU pipeline flush caused by memory order violation when exiting the loop, so as to improve the execution efficiency of the CPU.

  • Only one shared variable atomic operation can be guaranteed: when an operation is performed on a shared variable, we can use cyclic CAS to ensure atomic operation, but when multiple shared variables are operated, cyclic CAS cannot guarantee the atomicity of the operation. At this time, we can use locks, or a clever way is to merge multiple shared variables into a shared variable Measure to operate. For example, there are two shared variables i = 2,j=a. combine ij=2a, and then use CAS to operate ij. Since Java 1.5, JDK has provided AtomicReference class to ensure atomicity between reference objects. You can put multiple variables in one object for CAS operation.

Using locks to guarantee atomic operation

Or take the above column as the column. For ordinary counters, we only need to lock the counting method:

public synchronized void  add(int delta){
  this.count = count+delta;
}

The results are as follows:

normalCounter:1000000
safeCounter:1000000

Both counters get the right results

How does CPU realize atomic operation

Topics: Java JDK Spring jvm