In the last video I helped you beat the bully visibility of concurrency, and in this video we continue our crusade against atomicity, one of the three evils.

Explanation of order and atomicity

The ability for one or more operations to be executed without interruption by the CPU is called atomicity.

I understand that an operation is not divisible, that is, atomicity. In the context of concurrent programming, atomicity means that as soon as the thread starts performing the sequence of operations, it either executes all of them or none of them, with no half-execution allowed.

We try to compare database transactions with concurrent programming:

1. In the database

The concept of atomicity looks like this: a transaction is treated as an indivisible whole in which all operations are performed or not. If an error occurs during the transaction, it will be rolled back to the state before the transaction started, as if the transaction had not been executed. (I.e. : transactions are either executed or none are executed)

2. In concurrent programming

The concept of atomicity looks like this:

  • The first interpretation: a thread or process is executing without context switching.
    • Context switch: Switch the CPU from one process/thread to another process/thread (the premise of the switch is to obtain CPU usage).
  • Second understanding: we call atomicity the property that one or more operations in a thread (an indivisible whole) are not interrupted during CPU execution. (During execution, a context switch occurs whenever an interrupt occurs)

From the above description of atomicity, we can see that the concept of atomicity between concurrent programming and database is somewhat similar: both emphasize that an atomic operation cannot be interrupted!

And the non-atomic operation is illustrated like this:

After executing for A while, thread A tells the CPU to let thread B execute. There are many such operations in the operating system, sacrificing the very short time of switching threads to improve CPU utilization, thus improving system performance as a whole; This operation of the operating system is called a “time slice” switch.

First, the cause of the atomicity problem

Through the concept of atomicity described in the sequence, we conclude that the reason for the atomicity of shared variables between threads is context switching.

So let’s reproduce the atomicity problem with an example.

First define a bank account entity class:

@Data @AllArgsConstructor public static class BankAccount { private long balance; public long deposit(long amount){ balance = balance + amount; return balance; }}Copy the code

Then open multiple threads to deposit 1 yuan each time on this shared bank account:

import lombok.AllArgsConstructor; import lombok.Data; import java.util.ArrayList; /** * @author: mmzsblog * @description: Atomicity problem in concurrency * @date: 2020/2/25 14:05 */ public class AtomicDemo { public static final int THREAD_COUNT = 100; static BankAccount depositAccount = new BankAccount(0); public static void main(String[] args) throws Exception { ArrayList<Thread> threads = new ArrayList<>(); for (int i = 0; i < THREAD_COUNT; i++) { Thread thread = new DepositThread(); thread.start(); threads.add(thread); } for (Thread thread : threads) { thread.join(); } System. Out. Println (" Now the balance is "+ depositAccount. The getBalance () +" RMB "); } static class DepositThread extends Thread { @Override public void run() { for (int j = 0; j < 1000; j++) { depositAccount.deposit(1); // Deposit 1 yuan}}}} each timeCopy the code

Run the above program many times, each time the result is almost different, occasionally we can get the expected result 100*1*1000=100000 yuan, I listed several times the results:

The reason for this is because

balance = balance + amount;Copy the code

This code is not an atomic operation, where balance is a shared variable. Interruptions may occur in multithreaded environments. Thus the question of atomicity is starkly presented. As shown in the figure:

Of course, if balance were a local variable, this would not be a problem even in multithreaded situations (but the shared bank account would not be a local variable, otherwise it would not be a shared variable), because the local variable is private to the current thread. Just like the j variable in the for loop.

Welcome to pay attention to the public account “Java learning way “, check out more dry goods!

However, even with shared variables, I will never allow this problem, so we need to solve it and understand the atomicity problem in concurrent programming more deeply.

Second, solve the atomicity problem caused by context switch

2.1. Use local variables

The scope of local variables is inside the method, that is, when the method is finished executing, the local variable is discarded, and the local variable and the method live and die together. The frame of the call stack also lives and dies with the method, so it makes sense to put local variables in the call stack. In fact, local variables are put on the call stack.

Because each thread has its own call stack, local variables are stored in the call stack of each thread and are not shared, so there is no concurrency problem. It all boils down to this: No sharing, no mistakes.

But if we use local variables here, 100 threads save 1000 yuan each, and they all start saving from 0, they don’t accumulate, and they lose the result they want to show. Therefore, this method is not feasible.

Just as using a single thread here would guarantee atomicity, it would not solve the problem because it is not suitable for the current scenario.

2.2 Built-in atomicity guarantee

In Java, reads and assignments to variables of primitive data types are atomic operations.

For example, the following lines of code:

// atomicity a = true; // atomicity a = 5; // Set b to a (a = b); // set b to a (b = b); // A = b + 2; // a = b + 2; // a = b + 2; // Add the value of a to 1 // add the value of a to 1 // add the value of a to 1 // add the value of a to 1 // add the result to a a ++;Copy the code

2.3, synchronized

It would certainly be impossible to make all Java code atomic, and there is only so much a computer can do at a time. So when atomicity is not possible, we have to use a strategy to make the process look atomic. Hence synchronized.

Synchronized can guarantee both the visibility of operations and the atomicity of operation results.

Synchronized aMethod(){} prevents multiple threads from accessing synchronized methods on an object instance.

If an object has multiple synchronized methods, as long as one thread accesses one of the synchronized methods, no other thread can access any synchronized methods in the object at the same time.

So, here we just need to set the deposit method to synchronized to ensure atomicity.

 private volatile long balance;

 public synchronized long deposit(long amount){
     balance = balance + amount; //1
     return balance;
 }Copy the code

When synchronized is added, other threads cannot execute synchronized code until a thread executes the synchronized deposit method. Therefore, even if the execution of line 1 is interrupted, no other thread can access the variable balance; So at a macro level, the end result is correctness. But we don’t know if the middle operation is interrupted. For more details, see the CAS operation.

The balance variable is volatile. The balance variable is volatile. The purpose of the volatile keyword is to ensure that the balance variable is visible and that the synchronized block reads the latest value from main memory every time it enters.

So, here

 private volatile long balance;Copy the code

Synchronized can also be used

 private synchronized long balance;Copy the code

Because all of this guarantees visibility, as we covered in our first article, the weird concurrency of visibility.

2.4, the Lock Lock

public long deposit(long amount) { readWriteLock.writeLock().lock(); try { balance = balance + amount; return balance; } finally { readWriteLock.writeLock().unlock(); }}Copy the code

Lock ensures atomicity in a manner similar to synchronized.

Synchronized does not release a Lock. Synchronized does not release a Lock. The Java compiler automatically adds a lock() and unlock() to a synchronized method or block of code. Lock () and unlock() must come in pairs. After all, forgetting to unlock() is a fatal Bug (meaning other threads have to wait).

2.5. Types of atomic operations

If you want to use atomic classes to define attributes to ensure correctness of the results, you need to make the following changes to the entity class:

@Data @AllArgsConstructor public static class BankAccount { private AtomicLong balance; public long deposit(long amount) { return balance.addAndGet(amount); }}Copy the code

The JDK provides a number of atomic operation classes to ensure atomicity of operations. For example, the most common basic type:

AtomicBoolean
AtomicLong
AtomicDouble
AtomicIntegerCopy the code

At the bottom of these atomic operation classes is the CAS mechanism, which ensures that the entire assignment operation is atomic and cannot be broken, thus ensuring that the final result is correct.

In contrast to synchronized, atomic operation types are equivalent to guaranteeing atomicity from the micro level, whereas synchronized guarantees atomicity from the macro level.

In the 2.5 solution above, every small operation is atomic, such as atom class modification operations such as AtomicLong, whose OWN CRUD operations are atomic.

Now, does it mean that the whole thing is atomic just because every little operation is atomic?

Apparently not.

It still raises thread-safety issues, such as A method whose entire process is read A- read B- modify A- modify B- write A- write B; So, if after modifying A, the atomicity of the operation is lost and thread B starts to read B, the atomicity problem will occur.

Don’t assume that just because you use thread-safe classes, all of your code is thread-safe! It all starts with reviewing the overall atomicity of your code. Take this example:

@NotThreadSafe public class UnsafeFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNum = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); @Override public void service(ServletRequest request, ServletResponse response) { BigInteger tmp = extractFromRequest(request); if (tmp.equals(lastNum.get())) { System.out.println(lastFactors.get()); } else { BigInteger[] factors = factor(tmp); lastNum.set(tmp); lastFactors.set(factors); System.out.println(factors); }}}Copy the code

Although it uses atomic classes for all operations, the operations are not atomic. In other words: For example, after thread A executes lastNumber.set(TMP) in the else statement, another thread executes lastFactorys.get() in the if statement, and then thread A continues to execute lastfactors.set (factors) to update factors!

From this logical process, thread safety issues have already occurred.

It destroys the whole process of reading A- reading B- modifying A- modifying B- writing A- writing B. After writing A, other threads read B, resulting in that the B value read is not the B value after writing. And that’s where atomicity comes in.

Ok, so that concludes my understanding and summary of atomicity in union method, and from these two articles we have a general understanding of common visibility, atomicity problems in concurrency, and their common solutions.

The last

Post an example of atomicity that you often see.

Q: It’s often said that adding and subtracting long variables on 32-bit machines is a concurrency hazard. Is that true?

Answer: It is true that adding and subtracting long variables on 32-bit machines is a concurrency hazard.

The reason: atomicity problems with thread switching.

Non-volatile long and double variables are 8-byte 64-bit, and the 32-bit machine that reads or writes the variable must split it into two 32-bit operations. It is possible that one thread has read the higher 32 bits of a value, and the lower 32 bits have been changed by another thread. Therefore, it is recommended that the longdouble variable be declared volatile or synchronize to avoid concurrency problems.

Reference article:

  • 1, Geek time Java concurrent programming combat
  • 2, https://juejin.cn/post/6844903925749907463
  • 3, https://www.cnblogs.com/54chensongxia/p/12073428.html
  • 4, https://www.dazhuanlan.com/2019/10/04/5d972ff1e314a/


Welcome to the public account: The way of Java learning

Personal blog: www.mmzsblog.cn