Analysis of java Concurrent AtomicInteger Source Code

  • 2021-07-26 07:54:07
  • OfStack

Problem

(1) What is atomic operation?

(2) What is the relationship between atomic operation and ACID of database?

(3) How does AtomicInteger realize atomic operation?

(4) What are the disadvantages of AtomicInteger?

Brief introduction

AtomicInteger is an atomic class provided under java, which mainly operates on the integer type of int, and realizes atomic operation by calling CAS of the underlying Unsafe.

Remember Unsafe? Click the link to [java Unsafe Detailed Analysis]

Atomic operation

Atomic operations are operations that will not be interrupted by thread scheduling mechanisms. Once an operation starts, it runs straight to the end without any thread context switching.

Atomic operation can be one step or multiple operation steps, but its sequence can not be disturbed, nor can it be cut and only one part of it can be executed. It is the core feature of atomicity to regard the whole operation as a whole.

The atomic operation we are talking about here is related to the atomicity in the database ACID, I think the biggest difference is that the atomicity in the database is mainly used in transactions, and all update operations within a transaction are either successful or failed. Transactions have a rollback mechanism, while the atomic operations we say here have no rollback, which is the biggest difference.

Source code analysis

Primary attribute


//  Get Unsafe Example of 
private static final Unsafe unsafe = Unsafe.getUnsafe();
//  Identification value Offset of field 
private static final long valueOffset;
//  Static code block, through unsafe Get value Offset of 
static {
 try {
  valueOffset = unsafe.objectFieldOffset
   (AtomicInteger.class.getDeclaredField("value"));
 } catch (Exception ex) { throw new Error(ex); }
}
//  Storage int Type value, use the volatile Modify 
private volatile int value;

(1) Use value of int type to store values, and use volatile modification. volatile mainly ensures visibility, that is, one thread modification is immediately visible to another thread. The main implementation principle is memory barrier. If you don't expand here, you can consult relevant data by yourself.

(2) Call the objectFieldOffset () method of Unsafe to get the offset of the value field in the class for later CAS operations.

compareAndSet () method


public final boolean compareAndSet(int expect, int update) {
 return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// Unsafe Methods in 
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

Call the Unsafe. compareAndSwapInt () method implementation, which takes four parameters:

(1) The object of the operation;

(2) The offset of the fields in the object;

(3) The original value, that is, the expected value;

(4) The value to be modified;

As you can see, this is an native method, and the bottom layer is written using C/C + +, which is mainly implemented by calling CAS instruction of CPU. It can guarantee that it will only be updated when the field value at the corresponding offset is the expected value, that is, two-step operation like the following:


if(value == expect) {
 value = newValue;
}

The CAS instruction of CPU can ensure that these two steps are a whole, so there will be no problem that the value of value is a when comparison is possible in multithreaded environment, and the value of value may have become b when the value is really assigned.

getAndIncrement () method


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

// Unsafe Methods in 
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;
}

The underlying getAndIncrement () method is the getAndAddInt () method of the called Unsafe, which takes three parameters:

(1) The object of the operation;

(2) The offset of the fields in the object;

(3) The value to be increased;

Viewing the source code of getAndAddInt () method of Unsafe, we can see that it gets the current value first, and then calls compareAndSwapInt () to try to update the value at the corresponding offset. If it succeeds, it will jump out of the loop, if it does not succeed, it will try again until it succeeds. Isn't this an optimistic locking mechanism (CAS + spin)

The other methods in AtomicInteger are almost all similar, eventually calling compareAndSwapInt () to Unsafe to ensure atomicity of the value value update.

Summarize

(1) A variable value modified by volatile is maintained in AtomicInteger to ensure visibility;

(2) The main methods in AtomicInteger almost always end up calling the compareAndSwapInt () method in Unsafe to ensure the atomicity of variable modification.

Easter egg

(1) Why do you need AtomicInteger?

Let's look at an example:


public class AtomicIntegerTest {
 private static int count = 0;

 public static void increment() {
  count++;
 }

 public static void main(String[] args) {
  IntStream.range(0, 100)
    .forEach(i->
      new Thread(()->IntStream.range(0, 1000)
        .forEach(j->increment())).start());

  //  Use here 2 Or 1 Look at your own machine 
  //  I'm here to use run Running greater than 2 Will exit the loop 
  //  But with debug Running greater than 1 You will exit the loop 
  while (Thread.activeCount() > 1) {
   //  Give up CPU
   Thread.yield();
  }

  System.out.println(count);
 }
}

There are 100 threads here, and each thread increases count 1000 times. You will find that the results of each run are different, but they have one thing in common: they are all less than 100,000 times, so it is problematic to use int directly.

So, can volatile solve this problem?


private static volatile int count = 0;

public static void increment() {
 count++;
}

Unfortunately, volatile can't solve this problem, because volatile has only two functions:

(1) Ensure visibility, that is, the modification of variables by one thread is immediately visible by another thread;

(2) Instruction reordering is prohibited;

Here's a very important point, count + + is actually a two-step operation, the first step is to get the value of count, and the second step is to add 1 to its value.

With volatile, there is no guarantee that these two steps will not be interrupted by other thread scheduling, so there is no guarantee of atomicity.

This leads to the AtomicInteger we are talking about today, whose self-increment calls CAS of Unsafe and uses spin guarantee 1 to succeed, which can guarantee the atomicity of the two-step operation.


public class AtomicIntegerTest {
 private static AtomicInteger count = new AtomicInteger(0);

 public static void increment() {
  count.incrementAndGet();
 }

 public static void main(String[] args) {
  IntStream.range(0, 100)
    .forEach(i->
      new Thread(()->IntStream.range(0, 1000)
        .forEach(j->increment())).start());

  //  Use here 2 Or 1 Look at your own machine 
  //  I'm here to use run Running greater than 2 Will exit the loop 
  //  But with debug Running greater than 1 You will exit the loop 
  while (Thread.activeCount() > 1) {
   //  Give up CPU
   Thread.yield();
  }

  System.out.println(count);
 }
}

100,000 will always be printed here.


Related articles: