Starting with this section, we explore the Java concurrency toolkit java.util.concurrent. This section first introduces the most basic atomic variables and the underlying principles and thinking.
Atomic variables
What is an atomic variable? Why do you need them?
stay Understanding the synchronized section We introduced a Counter class that uses synchronized keywords to guarantee atomic update operations. The code is as follows:
public class Counter { private int count; public synchronized void incr(){ count ++; } public synchronized int getCount() { return count; } }
For the count++ operation, the cost of using synchronzied is too high. It needs to acquire the lock first, release the lock finally, wait when the lock can not be acquired, and there will be context switching of the thread, all of which need cost.
In this case, atomic variables can be used instead. The basic sub-variable types in Java concurrent packages are:
- Atomic Boolean: Atomic Boolean Type
- Atomic Integer: Atomic Integer Type
- Atomic Long: Atomic Long Type
- AtomicReference: Atomic Reference Type
This is our main class, in addition to these four classes, there are other classes, we will also give a brief introduction.
For Integer, Long and Reference types, there are corresponding array types:
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
In order to update the fields in an object atomically, there are the following classes:
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
AtomicReference has two similar classes, which are easier to use in some cases:
- AtomicMarkableReference
- AtomicStampedReference
You may find out, how come there are no atomic variables for char, short, float, double types? Probably less, if necessary, you can convert it to int/long and then use Atomic Integer or Atomic Long. For example, for floats, the following methods can be used to convert ints to each other:
public static int floatToIntBits(float value) public static float intBitsToFloat(int bits);
Next, let's look at a few basic subtypes, starting with Atomic Integer.
AtomicInteger
Basic Usage
Atomic Integer has two constructions:
public AtomicInteger(int initialValue) public AtomicInteger()
The first constructor gives an initial value, and the second one is zero.
Values in Atomic Integer can be obtained or set directly by:
public final int get() public final void set(int newValue)
It is called an atomic variable because it contains some methods of combining operations in an atomic way, such as:
//Get old values atomically and set new values public final int getAndSet(int newValue) //Get the old value atomically and add 1 to the current value public final int getAndIncrement() //Get the old value atomically and subtract 1 from the current value public final int getAndDecrement() //Get old values atomically and add current values delta public final int getAndAdd(int delta) //Add 1 to the current value atomically and get a new value public final int incrementAndGet() //Reduce the current value by 1 atomically and get a new value public final int decrementAndGet() //Adding the current value atomically delta And get new values public final int addAndGet(int delta)
The implementation of these methods depends on another public method:
public final boolean compareAndSet(int expect, int update)
This is a very important method, compare and set up, we will be referred to as CAS in the future. This method achieves the following functions atomically: if the current value is equal to expect, update to update, otherwise do not update, if the update is successful, return true, otherwise return false.
Atomic Integer can be used as a counter in a program. Multiple threads update concurrently, and it always achieves correctness. Let's take an example:
public class AtomicIntegerDemo { private static AtomicInteger counter = new AtomicInteger(0); static class Visitor extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { counter.incrementAndGet(); Thread.yield(); } } } public static void main(String[] args) throws InterruptedException { int num = 100; Thread[] threads = new Thread[num]; for (int i = 0; i < num; i++) { threads[i] = new Visitor(); threads[i].start(); } for (int i = 0; i < num; i++) { threads[i].join(); } System.out.println(counter.get()); } }
The output of the program is always correct, 10,000.
Basic Principles and Thinking
The use of Atomic Integer is simple and straightforward. How does it work? Its main internal members are:
private volatile int value;
Note that its declaration is volatile, which is necessary to ensure memory visibility.
Most of its update implementations are similar. Let's look at a method called incrementAndGet, which is coded as follows:
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
The body of the code is a dead loop. First, it gets the current value, calculates the expected value next, and then calls the CAS method to update it. If the current value does not change, it updates and returns the new value. Otherwise, it continues until the update is successful.
Compared with synchronized locks, this mode of atomic renewal represents a different way of thinking.
synchronized is pessimistic. It assumes that updates are likely to conflict, so the lock is acquired before it is updated. The updating logic of atomic variables is optimistic. It assumes that there are fewer conflicts, but using CAS updates, i.e. conflict detection, if there are conflicts, that's OK. Just keep trying.
synchronized represents a blocking algorithm. When the lock is not available, it enters the lock waiting queue and waits for other threads to wake up. It has context switching overhead. The updating logic of atomic variables is non-blocking. When updating conflicts occur, it will retry without blocking and context switching overhead.
For most of the simpler operations, this optimistic non-blocking approach outperforms the pessimistic blocking approach in both low-concurrency and high-concurrency scenarios.
Atomic variables are relatively simple, but for complex data structures and algorithms, non-blocking methods are often difficult to implement and understand. Fortunately, some non-blocking containers have been provided in Java concurrent packages. We just need to use them, for example:
- Concurrent LinkedQueue and Concurrent LinkedDeque: Non-blocking Concurrent Queue
- Concurrent SkipListMap and Concurrent SkipListSet: Non-blocking Concurrent Map and Set
These containers will be introduced in subsequent chapters.
But how does compareAndSet work? Let's look at the code:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
It calls unsafe's compareAndSwapInt method. What is unsafe? Its type is sun.misc.Unsafe, which is defined as:
private static final Unsafe unsafe = Unsafe.getUnsafe();
It's Sun's private implementation and, by name, represents "insecurity" and should not be used directly by ordinary applications. In principle, general computer systems directly support CAS instructions at the hardware level, and Java implementations will use these special instructions. From the point of view of program, we can regard compareAndSet as the basic operation of computer, just accept it directly.
Implementation lock
Based on CAS, in addition to optimistic non-blocking algorithms, it can also be used to implement pessimistic blocking algorithms, such as locks. In fact, all blocking tools, containers and algorithms in Java concurrent packages are also CAS-based (however, some other support is needed).
How to achieve it? Let's demonstrate a simple example of using Atomic Integer to implement a lock MyLock. The code is as follows:
public class MyLock { private AtomicInteger status = new AtomicInteger(0); public void lock() { while (!status.compareAndSet(0, 1)) { Thread.yield(); } } public void unlock() { status.compareAndSet(1, 0); } }
In MyLock, status is used to indicate the state of the lock, 0 is not locked, 1 is locked, lock()/unlock() is updated by CAS method, lock() only exits after the update is successful, which achieves the blocking effect. Generally speaking, this blocking method consumes too much CPU and has a more efficient way. We will introduce it in the following chapters. MyLock is just for demonstrating basic concepts, and classes in Java concurrent packages such as ReentrantLock should be used in actual development.
AtomicBoolean/AtomicLong/AtomicReference
The usage and principle of Atomic Boolean/Atomic Long/Atomic Reference are similar to Atomic Integer. Let's briefly introduce it.
AtomicBoolean
AtomicBoolean can be used to represent a flag bit in a program. Its atomic operation methods are as follows:
public final boolean compareAndSet(boolean expect, boolean update) public final boolean getAndSet(boolean newValue)
In fact, AtomicBoolean also uses int-type values internally, with 1 for true and 0 for false. For example, its CAS method code is:
public final boolean compareAndSet(boolean expect, boolean update) { int e = expect ? 1 : 0; int u = update ? 1 : 0; return unsafe.compareAndSwapInt(this, valueOffset, e, u); }
AtomicLong
Atomic Boolean can be used to generate unique serial numbers in programs in a way similar to Atomic Integer, which is not to be overlooked. Its CAS method calls another unsafe method, such as:
public final boolean compareAndSet(long expect, long update) { return unsafe.compareAndSwapLong(this, valueOffset, expect, update); }
AtomicReference
AtomicReference is used to update complex types atomically. It has a type parameter that requires specifying the type of reference to be used. The following code demonstrates its basic usage:
public class AtomicReferenceDemo { static class Pair { final private int first; final private int second; public Pair(int first, int second) { this.first = first; this.second = second; } public int getFirst() { return first; } public int getSecond() { return second; } } public static void main(String[] args) { Pair p = new Pair(100, 200); AtomicReference<Pair> pairRef = new AtomicReference<>(p); pairRef.compareAndSet(p, new Pair(200, 200)); System.out.println(pairRef.get().getFirst()); } }
The CAS method of AtomicReference calls another method of unsafe:
public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); }
Atomic array
Atomic arrays are easy to update every element in an array in atomic way. Let's take Atomic Integer Array as an example to give a brief introduction.
It has two construction methods:
public AtomicIntegerArray(int length) public AtomicIntegerArray(int[] array)
The first creates an empty array of length length. The second accepts an existing array, but does not directly manipulate it. Instead, it creates a new array, copying only the contents of the parameter array to the new array.
Atomic IntegerArray updates mostly have array index parameters, such as:
public final boolean compareAndSet(int i, int expect, int update) public final int getAndIncrement(int i) public final int getAndAdd(int i, int delta)
The first parameter i is the index. Let's take a simple example:
public class AtomicArrayDemo { public static void main(String[] args) { int[] arr = { 1, 2, 3, 4 }; AtomicIntegerArray atomicArr = new AtomicIntegerArray(arr); atomicArr.compareAndSet(1, 2, 100); System.out.println(atomicArr.get(1)); System.out.println(arr[1]); } }
Output is:
100 2
FieldUpdater
FieldUpdater is easy to update fields in an object atomically. FieldUpdater does not need to be declared as atomic variables. FieldUpdater is based on reflection mechanism. We will introduce reflection in subsequent chapters. Here's a brief introduction to its usage. See the code:
public class FieldUpdaterDemo { static class DemoObject { private volatile int num; private volatile Object ref; private static final AtomicIntegerFieldUpdater<DemoObject> numUpdater = AtomicIntegerFieldUpdater.newUpdater(DemoObject.class, "num"); private static final AtomicReferenceFieldUpdater<DemoObject, Object> refUpdater = AtomicReferenceFieldUpdater.newUpdater( DemoObject.class, Object.class, "ref"); public boolean compareAndSetNum(int expect, int update) { return numUpdater.compareAndSet(this, expect, update); } public int getNum() { return num; } public Object compareAndSetRef(Object expect, Object update) { return refUpdater.compareAndSet(this, expect, update); } public Object getRef() { return ref; } } public static void main(String[] args) { DemoObject obj = new DemoObject(); obj.compareAndSetNum(0, 100); obj.compareAndSetRef(null, new String("hello")); System.out.println(obj.getNum()); System.out.println(obj.getRef()); } }
There are two members num and ref in the class DemoObject, declared volatile, but not atomic variables. However, DemoObject provides an atomic update method compareAndSet, which is implemented using FieldUpdater corresponding to the field. FieldUpdater is a static member. It is obtained through the newUpdater factory method that the parameters needed by newUpdater are type, field name, reference type, and so on. Specific types that need to be referenced.
ABA problem
There is an ABA problem in using CAS to update. The problem is that a thread starts to see a value of A and then updates it with CAS. Its actual expectation is that no other thread has modified it before updating, but ordinary CAS can not, because in this process, other threads may have changed, for example, to B first, and then back to A.
ABA is not a problem related to the logic of the program. If it is a problem, one solution is to use Atomic Stamped Reference to add a timestamp to the value of the modification. Only when the value and timestamp are the same, can the modification be made. Its CAS method is declared as:?
public boolean compareAndSet( V expectedReference, V newReference, int expectedStamp, int newStamp)
For example:
Pair pair = new Pair(100, 200); int stamp = 1; AtomicStampedReference<Pair> pairRef = new AtomicStampedReference<Pair>(pair, stamp); int newStamp = 2; pairRef.compareAndSet(pair, new Pair(200, 200), stamp, newStamp);
Atomic Stamped Reference modifies two values simultaneously in compareAndSet. One is a reference and the other is a timestamp. How does it achieve atomicity? In fact, the internal Atomic StampedReference combines two values into one object and modifies a value. Let's look at the code:
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
This Pair is an internal class of Atomic StampedReference, whose members include references and timestamps, specifically defined as:
private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } }
Atomic StampedReference converts the combination comparison and modification of reference values and timestamps to the comparison and modification of individual values of this internal class Pair.
Atomic Markable Reference is another enhancement class of Atomic Reference. Similar to Atomic Stamped Reference, it also associates a field with a reference, but this time it is a boolean type flag, which is modified only if both the reference value and the flag bit are the same.
Summary
This section introduces the usage of various atomic variables and the underlying principle of CAS. For the need of counting and generating serial numbers in concurrent environment, CAS is the basis of Java concurrent package. Based on its efficient, optimistic and non-blocking data structure and algorithm, CAS is also the basis of lock, synchronization tool and various containers in concurrent package.
In the next section, we discuss explicit locks in concurrent packages.
(As in other chapters, all the code in this section is located at https://github.com/swiftma/program-logic)
----------------
To be continued, check the latest articles, please pay attention to the Wechat public number "Lao Ma Says Programming" (scanning the two-dimensional code below), from the entry to advanced, in-depth shallow, Lao Ma and you explore the essence of Java programming and computer technology. Be original and reserve all copyright.