JAVA concurrent programming: CAS and AQS

Keywords: Java Programming JDK REST

When it comes to JAVA concurrent programming, you have to talk about CAS(Compare And Swap) and AQS (AbstractQueued Synchronizer).

CAS(Compare And Swap)

What is CAS?

CAS(Compare And Swap), that is, compare and exchange. CAS operation consists of three operands: memory location (V), expected original value (A) and new value (B). If the value of the memory location matches the expected original value, the processor automatically updates the location value to a new value. Otherwise, the processor does nothing. In either case, it returns the value of that location before the CAS instruction. CAS effectively states that "I think location V should contain value A; if it contains value, place B in this position; otherwise, don't change the location, just tell me the current value of this location.

In JAVA, the sun.misc.Unsafe class provides hardware-level atomic operations to implement this CAS. A large number of classes under the java.util.concurrent package use the CAS operation of this Unsafe.java class. The specific implementation of Unsafe. Java is not discussed here.

Typical application of CAS

Classes under the java.util.concurrent.atomic package are mostly implemented using CAS operations (eg. AtomicInteger.java,AtomicBoolean,AtomicLong). The implementation of these atomic classes is outlined below in terms of partial implementations of AtomicInteger.java.

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    private volatile int value;// Initial int size
    // Some code is omitted.

    // With a parameter constructor, you can set the initial int size
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    // No parametric constructor, initial int size is 0
    public AtomicInteger() {
    }

    // Get the current value
    public final int get() {
        return value;
    }

    // Set the value to newValue
    public final void set(int newValue) {
        value = newValue;
    }

    //Returns the old value and sets the new value to newValue
    public final int getAndSet(int newValue) {
        /**
        * Here, the for loop is used to set new values continuously through CAS operations.
        * CAS The relationship between implementation and locking implementation is somewhat similar to that between optimistic locking and pessimistic locking.
        * */
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
    }

    // The new value of the atom is update, and expect is the expected current value.
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    // Get the current value and set the new value to current+1
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

    // Some of the code is omitted here, and the rest of the code is roughly implemented in a similar way.
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

Generally speaking, when the competition is not particularly fierce, the atomic operation performance under this package is much more efficient than using the synchronized keyword (see getAndSet(), which shows that if the resource competition is very fierce, the for cycle may last for a long time and can not be successfully jumped out. However, this situation may need to consider reducing resource competition.  
In many scenarios, we may use these atomic operations. A typical application is counting. Thread security needs to be considered in the case of multi-threading. Usually the first image may be:

public class Counter {
    private int count;
    public Counter(){}
    public int getCount(){
        return count;
    }
    public void increase(){
        count++;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

The above class will have thread security problems in multi-threaded environment. The simplest way to solve this problem may be through locking, adjusting as follows:

public class Counter {
    private int count;
    public Counter(){}
    public synchronized int getCount(){
        return count;
    }
    public synchronized void increase(){
        count++;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

This is similar to the implementation of pessimistic lock. I need to get this resource, so I lock it. No other thread can access the resource until I release the lock on the resource after I finish the operation. We know that pessimistic locks are not as efficient as optimistic locks. The implementation of atomic classes under Atomic is similar to optimistic locks. It is more efficient than using synchronized relational words.

public class Counter {
    private AtomicInteger count = new AtomicInteger();
    public Counter(){}
    public int getCount(){
        return count.get();
    }
    public void increase(){
        count.getAndIncrement();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

AQS(AbstractQueuedSynchronizer)

What is AQS?

AQS (AbstractQueued Synchronizer), AQS is a synchronization framework provided by JDK to implement blocking lock based on FIFO waiting queue and related synchronizers. This abstract class is designed as the base class of synchronizers that can be represented by atomic int values. If you've seen a source implementation like CountDownLatch, you'll find that there's an internal class Sync that inherits AbstractQueued Synchronizer. It can be seen that CountDownLatch is a synchronizer based on AQS framework. There are many similar synchronizers under JUC. (eg. Semaphore)

AQS usage

As mentioned above, AQS manages a single integer about state information that can represent any state. For example, Semaphore uses it to represent the remaining number of licenses, ReentrantLock uses it to indicate how many locks have been requested by the thread that owns it, and FutureTask uses it to represent the state of the task (not yet started, run, completed, and cancelled).

 To use this class as the basis of a synchronizer, redefine the
 * following methods, as applicable, by inspecting and/or modifying
 * the synchronization state using {@link #getState}, {@link
 * #setState} and/or {@link #compareAndSetState}:
 *
 * <ul>
 * <li> {@link #tryAcquire}
 * <li> {@link #tryRelease}
 * <li> {@link #tryAcquireShared}
 * <li> {@link #tryReleaseShared}
 * <li> {@link #isHeldExclusively}
 * </ul>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

As mentioned in the JDK document, using AQS to implement a synchronizer requires overriding the following methods, and using getState,setState,compareAndSetState to set the acquisition state.
1. boolean tryAcquire(int arg) 
2. boolean tryRelease(int arg) 
3. int tryAcquireShared(int arg) 
4. boolean tryReleaseShared(int arg) 
5. boolean isHeldExclusively()

The above methods need not be fully implemented. Different methods can be chosen according to the types of locks acquired. Synchronizers supporting exclusive acquisition of locks should implement tryAcquire, tryRelease, isHeldExclusively, while synchronizers supporting shared acquisition should implement tryAcquireShared, tryReleaseShared and isHeldExclusively. Following is an example of CountDownLatch implementing synchronizer based on AQS. CountDownLatch holds the current count with synchronization state, and countDown method calls release, which results in the decrease of the counter; when the counter is 0, all threads are released; when await calls acquire, if the counter is 0, acquire will return immediately, otherwise it will block. Usually used in situations where a task needs to wait for other tasks to complete before it can continue to execute. The source code is as follows:

public class CountDownLatch {
    /**
     * Internal Sync Based on AQS
     * Use AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            // Use AQS's getState() method to set state
            setState(count);
        }

        int getCount() {
            // Getting state using the getState() method of AQS
            return getState();
        }

        // Overlay attempts to acquire locks in shared mode
        protected int tryAcquireShared(int acquires) {
            // Here, the success is expressed by whether the state state is zero. When the state is zero, return 1 can be obtained, otherwise - 1 can not be returned.
            return (getState() == 0) ? 1 : -1;
        }

        // Overlay attempts to release locks in shared mode
        protected boolean tryReleaseShared(int releases) {
            // Decrement count in for loop until successful;
            // When the state value is count0, return false to indicate signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

    // Constructing CountDownLatch with a given count value
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    // Block the current thread until count becomes zero or the thread is interrupted
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    // Block the current thread unless count becomes zero or the time to wait for timeout. When count becomes zero, return true
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    // count decline
    public void countDown() {
        sync.releaseShared(1);
    }

    // Get the current count value
    public long getCount() {
        return sync.getCount();
    }

    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

This article roughly talks about these things, and some of them are not particularly good. There are also some deficiencies. There are still many things about AQS. I suggest that you go to see the implementation of various classes under JUC and cooperate with the book JAVA Concurrent Programming Practice. I believe it can be seen clearly, so as to get a deeper understanding.

Posted by TGLMan on Sun, 12 May 2019 00:41:42 -0700