Prerequisites for the emergence of atomic operations
Let's take a look at this example to help you understand how atomic operations work:
public class Counter {
int count;
public void increment() {
count++;
}
}
When we have one thread, everything works great, but if we add multithreading, we get wrong results, and all because the increment operation is not one operation, but three: a request to get the current valuecount, then increment it by 1 and write again tocount.
And when two threads want to increment a variable, you will most likely lose data. That is, both threads receive 100, as a result, both will write 101 instead of the expected value of 102.
And how to solve it? You need to use locks. The synchronized keyword helps solve this problem, using it gives you the guarantee that one thread will access the method at a time.
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
Plus, you need to add the volatile keyword , which ensures the correct visibility of references among threads. We have reviewed his work above.
But still there are downsides. The biggest one is performance, at that point in time when many threads are trying to acquire a lock and one gets a write opportunity, the rest of the threads will either be blocked or suspended until the thread is released.
All these processes, blocking, switching to another status are very expensive for system performance.
Atomic operations
The algorithm uses low-level machine instructions such as compare-and-swap (CAS, compare-and-swap, which ensures data integrity and there is already a large amount of research on them).
A typical CAS operation operates on three operands:
- Memory space for work (M)
- Existing expected value (A) of a variable
- New value (B) to be set
CAS atomically updates M to B, but only if the value of M is the same as A, otherwise no action is taken.
In the first and second cases, the value of M will be returned. This allows you to combine three steps, namely, getting the value, comparing the value, and updating it. And it all turns into one operation at the machine level.
The moment a multi-threaded application accesses a variable and tries to update it and CAS is applied, then one of the threads will get it and be able to update it. But unlike locks, other threads will simply get errors about not being able to update the value. Then they will move on to further work, and switching is completely excluded in this type of work.
In this case, the logic becomes more difficult due to the fact that we have to handle the situation when the CAS operation did not work successfully. We'll just model the code so that it doesn't move on until the operation succeeds.
Introduction to Atomic Types
Have you come across a situation where you need to set up synchronization for the simplest variable of type int ?
The first way we've already covered is using volatile + synchronized . But there are also special Atomic* classes.
If we use CAS, then operations work faster compared to the first method. And in addition, we have special and very convenient methods for adding a value and increment and decrement operations.
AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray are classes in which operations are atomic. Below we will analyze the work with them.
AtomicInteger
The AtomicInteger class provides operations on an int value that can be read and written atomically, in addition to providing extended atomic operations.
It has get and set methods that work like reading and writing variables.
That is, “happens-before” with any subsequent receipt of the same variable that we talked about earlier. The atomic compareAndSet method also has these memory consistency features.
All operations that return a new value are performed atomically:
int addAndGet (int delta) | Adds a specific value to the current value. |
boolean compareAndSet(expected int, update int) | Sets the value to the given updated value if the current value matches the expected value. |
int decrementAndGet() | Decreases the current value by one. |
int getAndAdd(int delta) | Adds the given value to the current value. |
int getAndDecrement() | Decreases the current value by one. |
int getAndIncrement() | Increases the current value by one. |
int getAndSet(int newValue) | Sets the given value and returns the old value. |
int incrementAndGet() | Increases the current value by one. |
lazySet(int newValue) | Finally set to the given value. |
boolean weakCompareAndSet(expected, update int) | Sets the value to the given updated value if the current value matches the expected value. |
Example:
ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
System.out.println(atomicInteger.get()); // prints 50
GO TO FULL VERSION