Concurrent programming: Atomic
Hello, I'm Xiao Hei, a migrant worker who lives on the Internet.
Before we begin today's content, let's ask a question: is it thread safe to use int type for addition and subtraction? For example, is there a problem with operations such as I + +, + I, i=i+1 in the case of concurrency?
Let's see by running the code.
public class AtomicDemo { public static void main(String[] args) throws InterruptedException { Data data = new Data(); Thread a = new Thread(() -> { for (int i = 0; i < 100000; i++) { System.out.println(Thread.currentThread().getName()+"_"+data.increment()); } }, "A"); Thread b = new Thread(() -> { for (int i = 0; i < 100000; i++) { System.out.println(Thread.currentThread().getName()+"_"+data.increment()); } }, "B"); a.start(); b.start(); // Wait for threads A and B to complete execution a.join(); b.join(); System.out.println(data.getI()); } } class Data { private volatile int i = 0; public int increment() { i++; return i; } public int getI() { return i; } }
The above code is relatively simple. Threads a and B execute + + operations on i in the Data object at the same time, 100000 times respectively, and the final output. If i + + operation is thread safe, the final output result should be 200000, but we will see the following results when running the code:
We found that the final output is not 200000, but 199982. If it is executed several times, the result will change, and it will not be 200000 in most cases. This is mainly because the + + operation of int type is not atomic, and i + + is equal to i=i+1, that is, the step of adding 1 and re assigning i are not completed at the same time, so we conclude that the operation of int type is not thread safe.
In many practical scenarios, it is necessary to perform concurrent operations on a data, such as the deduction of the quantity of a commodity in the second kill activity of e-commerce. What should we do to ensure security?
The first thing we can think of is to lock the method of increment() with the synchronized keyword, so that only one thread can access it at a time.
However, in the previous article, we mentioned that synchronized is a heavyweight pessimistic lock. The concurrency of our business scenario may occur for a period of time. In most cases, there may not be a lot of competition, so is there a better way to deal with it? The answer is through AtomicInteger.
AtomicInteger
AtomicInteger is Java util. concurrent. A class in the atomic package. We look at the description of this package in the official document and say that it is a toolkit that supports lockless thread safe programming on a single variable. As we expect, it achieves thread safety without locking.
Let's modify the code of the above example.
class Data { private volatile AtomicInteger i = new AtomicInteger(0); public int increment() { return i.incrementAndGet(); } public int getI() { return i.get(); } }
Very simply, change the original int to AtomicInteger, and call the incrementandget () method when performing the increment() method to add. Similarly, when we run the code, we will find that no matter how many times we run it, the final execution result of the code is the same, 200000. So we say AtomicInteger is thread safe. In addition to the incrementAndGet() method, there are many other operations, such as decrementAndGet(), getAndIncrement(), getanddecrease(), getAndAdd(int delta), addAndGet(int delta), etc. in fact, they are atomic implementations of i + +, + i, i=i+n, i+=n.
In addition to AtomicInteger, Java util. concurrent. There are other types in the atomic package, such as AtomicBoolean, AtomicLong, and so on.
Implementation principle
So how does AtomicInteger ensure atomicity without using synchronized? Let's take a look at the source code.
public class AtomicInteger extends Number implements java.io.Serializable { private static final Unsafe unsafe = Unsafe.getUnsafe(); // Value the address offset value in memory private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } // Guaranteed memory visibility with value volatile private volatile int value; public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } } public final class Unsafe { public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { // Obtain the volatile Int and ensure that the obtained value is the latest var5 = this.getIntVolatile(var1, var2); // compareAndSwapInt compares and exchanges native methods } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } }
Through the source code, we see that we call the Unsafe class getAndAddInt method in the incrementAndGet() method, and perform compareAndSwapInt operation on value inside this method. Through this method name, we can see that it is comparison and exchange, that is, CAS we mentioned earlier. That is, when performing the assignment operation, first check whether the current value is the value I added before. If not, I will add it again and then compare it. It is a cyclic process, which is also called spin.
Although CAS efficiently solves atomic operations, it still has three problems. In actual development, we must pay attention to using it in combination with our own actual business scenarios.
ABA problem
What is the ABA problem? Generally speaking, it is your uncle or your uncle. Your aunt is no longer your aunt~
What do you mean? After thread 1 gets a, another thread 2 changes a into B and A. when thread 1 modifies the value for CAS comparison, it finds that the value is still a, which is the same as that obtained by itself. However, during this process, this a has changed. It's like a thief who steals other people's money and then returns it. Is it still the original money? Although your money hasn't changed, the thief has broken the law, and you don't know it yet.
To solve this problem, a class is provided in the atomic package. Let's see how to solve it.
public static void main(String[] args) throws InterruptedException { AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); new Thread(() -> { try { int stamp = ref.getStamp(); String reference = ref.getReference(); System.out.println("Value obtained by thread 1:" + reference + " stamp:" + stamp); // sleep 2 seconds to simulate thread switching to 2 TimeUnit.SECONDS.sleep(2); boolean success = ref.compareAndSet(reference, "C", stamp, stamp + 1); System.out.println(Thread.currentThread().getName() + " " + success); } catch (InterruptedException e) { e.printStackTrace(); } }, "Thread 1").start(); new Thread(() -> { // Change to B first int stamp = ref.getStamp(); String reference = ref.getReference(); System.out.println("Value obtained by thread 2:" + reference + " stamp:" + stamp); ref.compareAndSet(reference, "B", stamp, stamp + 1); // Change back to A stamp = ref.getStamp(); reference = ref.getReference(); System.out.println("Value obtained by thread 2:" + reference + " stamp:" + stamp); ref.compareAndSet(reference, "A", ref.getStamp(), stamp + 1); }, "Thread 2").start(); }
We can see that the compareAndSet() method of AtomicStampedReference has four parameters:
- expectedReference: indicates the expected reference value
- newReference: indicates the new reference value to be modified
- expectedStamp: indicates the expected stamp (version number)
- newStamp: indicates the new stamp (version number) after modification
What do you mean? When modifying, you should not only compare whether the value is the same as the obtained value, but also compare the version number. In this way, the version number is incremented by 1 during each operation. Even if the value is changed from A to B and then back to A, the version number is changed from 0 to 1 and then to 2 without changing back to 0, so the ABA problem can be avoided.
Longer cycle time
When the concurrency is very large, using CAS, some threads may fail to modify the loop all the time, resulting in longer loop time and great execution overhead to the CPU. Moreover, since the reference in AtomicReference is volatile, in order to ensure memory visibility, cache consistency needs to be ensured and data is transmitted through the bus. When there are a large number of CAS cycles, a bus storm will occur.
Atomic operation of only one variable can be guaranteed
The third problem of CAS is that only one variable can be stored in AtomicReference. If you need to ensure the atomicity of multiple variable operations, you can't do it. In this case, you can only use the Lock tool in the synchronized or juc package.
Summary
To make a brief summary, using int type has thread safety problems in concurrent scenarios. AtomicInteger can be used to ensure atomic operation. Atomic is thread safe without lock through CAS. However, CAS has three problems. First, ABA problem can be solved through AtomicStampedReference; Second, in the case of fierce competition, the cycle time will become longer, resulting in bus storm; Third, the atomic operation of only one variable can be guaranteed.
In the specific business scenario, whether to use synchronized, Lock and other locking tools or Atomic CAS Lock free operation should be considered in combination with the scenario.
OK, that's all for today. I'll see you next time.
Pay attention to the official account [Java] dry cargo continues to ~