Atomic issues with Java memory models

Keywords: Java Programming Spring JDK

This blog series is a summary of the learning process of concurrent programming.Because there are many articles and the writing time is scattered, I organized a catalog paste (portal) for easy reference.

Concurrent Programming Series Blog Portal

Preface

As mentioned in previous articles, JMM is a reflection of the memory model specification in the Java language.JMM ensures atomicity, visibility and orderliness of read and write to shared variables in a multicore CPU multithreaded programming environment.

In this paper, we will talk specifically about how JMM ensures the atomicity of shared variable access.

Atomic problems

Atomicity means that one or more operations are performed either in full and without interruption by any factor or in total.

Here's a code that has atomic problems:

public class AtomicProblem {

    private static Logger logger = LoggerFactory.getLogger(AtomicProblem.class);
    public static final int THREAD_COUNT = 10;

    public static void main(String[] args) throws Exception {
        BankAccount  sharedAccount = new BankAccount("account-csx",0.00);
        ArrayList<Thread> threads = 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 < 1000 ; j++) {
                        sharedAccount.deposit(10.00);
                    }
                }
            });
            thread.start();
            threads.add(thread);
        }
        for (Thread thread : threads) {
            thread.join();
        }
        logger.info("the balance is:{}",sharedAccount.getBalance());
    }


    public static class BankAccount {
        private String accountName;

        public double getBalance() {
            return balance;
        }

        private double balance;

        public BankAccount(String accountName, double balance){
            this.accountName = accountName;
            this.balance =balance;
        }
        public double deposit(double amount){
            balance = balance + amount;
            return balance;
        }
        public double withdraw(double amount){
            balance = balance - amount;
            return balance;
        }
        public String getAccountName() {
            return accountName;
        }
        public void setAccountName(String accountName) {
            this.accountName = accountName;
        }
    }
}

The code above opens 10 threads, each of which makes 1,000 deposits to a shared bank account, each of which deposits 10 blocks, so in theory, the money in the last bank account should be 10 * 1000 * 10 = 100,000.I executed the above code several times and many times the final result was 100,000, but a few times the result was not what we expected.

14:40:25.981 [main] INFO com.csx.demo.spring.boot.concurrent.jmm.AtomicProblem - the balance is:98260.0

The reason for this is that the following is not an atomic operation, where balance is a shared variable.Interrupts may occur in a multithreaded environment.

balance = balance + amount;

The assignment operation above is divided into multiple steps, and the following is a simple analysis of how two threads add 10 to the balance at the same time (simulate the deposit process, assuming the balance has an initial value of 0)

Thread 1 loads balance from initial value 0 in shared memory to working memory
 Thread 1 adds 10 to the value in working memory

//Thread 1 runs out of CPU time and Thread 2 gets execution opportunities

Thread 2 loads balance from its initial value in shared memory to working memory, and the balance is still 0
 Thread 2 adds 10 to the value in working memory, where the copy value in working memory of thread 2 is 10
 Thread 2 flushes the replica value of balance back to shared memory, where the value of balance in shared memory is 10

//Thread 2 CPU time slice exhausted, Thread 1 gets execution opportunity again
 Thread 1 flushes the copy value from working memory back to shared memory, but at this point the copy value is still 10, so the last shared memory value is 10

The above is a simple simulation of a process in which an atomic problem results in an error in the end result of the program.

JMM's Assurance of Atomicity

Self-contained Atomicity Assurance

In Java, reading and assigning variables of a basic data type is atomic.

a = true;  //Atomicity
a = 5;     //Atomicity
a = b;     //Non-Atomic, done in two steps, loading the value of b in the first step and assigning b to a in the second step
a = b + 2; //Non-Atomic, done in three steps
a ++;      //Non-Atomic, done in three steps

synchronized

Synchronized guarantees the atomicity of the operation results.The principle of synchronized guaranteeing atomicity is also simple because synchronized prevents multiple threads from concurrently executing a piece of code.Or use the scenario of deposits above as a column. We just need to set the deposit method to synchronized to guarantee atomicity.

 public synchronized double deposit(double amount){
     balance = balance + amount; //1
     return balance;
 }

With synchronized, this code cannot be executed by other threads until one thread has finished executing deposit.In fact, we found that synchronized could not program atomic operations on code 1 above. It is possible that code 1 above would be interrupted, but even if other threads were interrupted, they would not be able to access the shared variable balance, and the results would be correct if the previously interrupted threads continued execution.

Therefore, synchronized guarantees the atomicity from the final result, that is, it only guarantees the final result is correct, and there is no guarantee whether the intermediate operation is interrupted or not.This and CAS operations need to be compared.

Lock Lock

public double deposit(double amount) {
    readWriteLock.writeLock().lock();
    try {
        balance = balance + amount;
        return balance;
    } finally {
        readWriteLock.writeLock().unlock();
    }
}

Lock locks guarantee atomicity in a similar way to synchronized, which is not covered here.

Atomic Operation Type

public static class BankAccount {
    //Omit other code
    private AtomicDouble balance;

    public double deposit(double amount) {
        return balance.addAndGet(amount);
    }
    //Omit other code
} 

JDK provides many classes of atomic operations to ensure the atomicity of operations.The underlying class of atomic operations uses the CAS mechanism, which is essentially different from synchronized assurance of atomicity.The CAS mechanism guarantees that the entire assignment operation cannot be interrupted by atoms, while the synchronized value guarantees the correctness of the final execution result of the code, that is, synchronized can eliminate the influence of atomic problems on the final execution result of the code.

A brief summary

In a multithreaded programming environment (whether multicore or single-core), access to shared variables is atomically problematic.This problem may result in incorrect execution of the program.JMM mainly provides the following ways to ensure that the atoms of the operation are not affected by atomic problems.

  • synchronized mechanism: to ensure the ultimate correctness of the program, is that the program is not affected by atomic problems;
  • Lock interface: similar to synchronized;
  • Atomic Operations Class: The underlying CAS mechanism ensures true atomicity for operations.

Posted by wilded1 on Fri, 20 Dec 2019 22:45:08 -0800