Java concurrency_ cas,aqs

Posted by deane034 on Fri, 04 Mar 2022 20:09:59 +0100

To sum up, cas (compare and swap) can be understood literally. Each time, it gets the old value from a certain memory area and compares it with the expected value. If it is equal, it is modified to a new value. If it is not equal, it will spin (While(! compareAndSwap(old,current)). Here is my website for learning spin lock

https://blog.csdn.net/qq_34337272/article/details/81252853

Of course, there are two problems with spin locks:

  1. Because spin lock is an optimistic lock, it will remain in the optional state when the current expected value and memory value are not equal. Obviously, when the thread competition is fierce, the long-term unsuccessful spin will occupy a high cpu (which can be solved by setting the number of spins)
  2. Unable to re-enter. Imagine that cas will still fail when the thread holding the lock wants to acquire the lock again. (it can be solved by introducing counters)
public void lock() {
			Thread cur = Thread.currentThread();
			if(cur == cas.get()){
				//If this is the thread, the counter++
				count++;
				return;
			}
			//Not this thread, cas gets lock
			while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }		
    }
    public void unlock() {
        Thread cur = Thread.currentThread();
        if (cur == cas.get()) {
            if (count > 0) {// If it is greater than 0, it indicates that the current thread has acquired the lock for many times. Releasing the lock is simulated by count ing minus one
                count--;
            } else {// If count==0, the lock can be released, so that the number of times to obtain the lock is consistent with the number of times to release the lock.
                cas.compareAndSet(cur, null);
            }
        }
    }

The following summary comes from

https://blog.csdn.net/vanchine/article/details/111772190?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-0&spm=1001.2101.3001.4242

AQS (AbstractQueuedSynchronizer) provides a set of synchronizer framework for multi-threaded access to shared resources. Many synchronization class implementations rely on it, such as ReentrantLock/Semaphore/CountDownLatch. It maintains a volatile int state. The thread determines whether the current thread successfully obtains the synchronization state (that is, cas operation on the state) by modifying (adding / subtracting the specified number) the state. At the same time, it also maintains a FIFO queue (first in first out queue). When the cas fails, the fair lock will choose to put the thread into the queue; The non fair lock will continue to spin.

Exclusive and shared locks
AQS supports two modes to obtain synchronization status: exclusive and shared. Exclusive mode allows only one thread to obtain synchronization status at the same time, while shared mode allows multiple threads to obtain synchronization status at the same time. ReentrantLock is a mutually exclusive lock implemented exclusively. The read lock of ReadWriteLock is a shared lock (obviously, reading data does not modify data, so it is not necessary to be exclusive), the write lock is an exclusive lock, and Semaphore is a shared lock (state is set to the number of accessible threads).

Take ReentrantLock as an example:

//Specify lock type
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
//Default to non fair lock
public ReentrantLock() {
    sync = new NonfairSync();
}
//Realize fair lock
private ReentrantLock reentrantLock = new ReentrantLock(true);

In terms of Synchronized, it is an unfair lock. Unlike ReentrantLock, which implements thread scheduling through AQS, there is no way to turn it into a fair lock.

The following is an analysis of some of the ReentranLock source code:

ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock();

--FairSync
//Fair competition in lock
final void lock() {
	//All compete for locks according to FIFO
    acquire(1);
}

--UnFairSync
//Competitive lock in unfair lock
final void lock() {
ยท	//Any thread can preempt the lock through CAS first
    if (compareAndSetState(0, 1))
    	//Set exclusive lock
    	setExclusiveOwnerThread(Thread.currentThread());
    else
    	//If preemption fails, the lock is competed according to FIFO
    	acquire(1);
}

--AbstractQueuedSynchronizer
//Releasing the lock is an implementation
public final boolean release(int arg) {
    if (tryRelease(arg)) {
    	// Node is a node that contains thread information and is placed in the queue
        Node h = head;
        if (h != null && h.waitStatus != 0)
        	//Release pending status
        	unparkSuccessor(h);
        return true;
    }
    return false;
}

The acquire method comes from AQS implementation:

public final void acquire(int arg) {
    //tryAcquire can be understood as an attempt to obtain an exclusive lock directly
    //After tryAcquire fails, add the encapsulated Node of the current thread to the end of AQS queue (addWaiter)
    ///And continue to cycle to acquire locks (acquirequeueueueueueueueued)
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        )
        selfInterrupt();
}

--FairSync
protected final boolean tryAcquire(int acquires) {
	...
	final Thread current = Thread.currentThread();
	int c = getState();
    if (c == 0) {
    	//hasQueuedPredecessors() first determines whether there are head and tail nodes
    	//Then judge whether there is a successor node, and finally judge whether the successor node is the current thread
    	// hasQueuedPredecessors() returns False when judging success
   		//return h !=  T & & whether there are head and tail nodes
   		//((s = h.next) == null | | whether there is a successor node
   		// s.thread != Thread.currentThread() );  Is it the current thread
    	//If yes, CAS needs to be continued
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }	...
}
--UnFairSync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //Get the status value. If it is 0, it means that CAS can still be used again
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
        	//Sets the thread that currently holds the lock
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //If the status is not 0, continue to judge whether the thread obtaining the lock is the current thread
    else if (current == getExclusiveOwnerThread()) {
    	//If yes, add 1 to status to increase the number of reentries
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //If none, return false
    return false;
}

//If TryAcquire fails, it needs to be added to the end of the queue
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //Get the prev (precursor) node of the current node first
            final Node p = node.predecessor();
            //If the precursor node is head, try to acquire the lock again
            if (p == head && tryAcquire(arg)) {
                //After obtaining the lock, set the head to the current node and clear the thread information and prev of the node
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //Failed to acquire lock. Judge whether to suspend
            if (shouldParkAfterFailedAcquire(p, node) &&
                //The status of the precursor node is SIGNAL, and the current thread park is blocked
                //LockSupport.park/unpark provides a switch like function for threads
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            //To cancel the lock acquisition is to set the waitStatus of the node to cancelled and clear the contents of the node
            //The most important thing is that unpark is released for the next thread
            cancelAcquire(node);
    }
}



Topics: Java Multithreading