J.U.C Parsing and Interpretation 1 (Implementation of Lock)

Keywords: Java less

Preface

To save you time, let me briefly introduce this article.This article is divided into three main parts: the implementation of Lock, the origin of AQS (through evolution), the use and principle analysis of three major JUC tool classes.

  • Lock implementation: Briefly introduce the implementation of classic Lock under ReentrantLock and ReentrantReadWriteLock JUC, and understand its implementation principle through handwritten simplified ReentrantLock and ReentrantReadWriteLock.

  • Origin of AQS: AQS is obtained by iterating over two simplified versions of Lock.Finally, Lock implements the Lock interface under J.U.C, which can either use our evolved AQS or dock AQS under JUC.This can help you understand AQS on the one hand, and on the other hand, you can learn how to use AQS to implement a custom Lock.Here, the understanding of the three Lock tools under the subsequent JUC is very helpful.

  • JUC three tools: After the first two parts of the study, this part is not too easy.It is easy to understand the internal operation and implementation of CountDownLatch, Semaphore, and CyclicBarrier.

However, because these three pieces of content are more, I divide them into three sub-articles to discuss.

1. Introduction

Lock

Lock interfaces are located in the locks package under J.U.C, which defines the methods Lock should have.

Lock method signature:

  • void lock(): Acquire locks (endless, wait until you can't get them)
  • boolean tryLock(): Acquire a lock (just taste it and don't get it)
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException: Acquire locks (obsolete, do not get locks for a certain period of time, even if)
  • void lockInterruptibly() throws InterruptedException: Acquire locks (at your disposal, xxx)
  • void unlock(): release lock
  • Condition newCondition(): Gets the Condition object

ReentrantLock

brief introduction

ReentrantLock is a reentrant lock, a pessimistic lock, and the default is an unfair lock (but it can be set to a fair lock through a Constructor).

Lock Application

ReentrantLock obtains the lock object through a construction method.The lock.lock() method is used to lock the current thread, and the lock.unlock() method is used to release the lock on the current thread.

Condition application

adopt

    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

Gets the Condition object (Condition is the interface under the locks package under J.U.C).

With the.await(*) of the Condition object, you can switch the thread state of the current thread to the Waiting state (or the Time Waiting state if you have parameters).Conversely, methods such as.signal(),.signalAll() restore the thread state to the Runnable state.

ReentrantReadWriteLock

brief introduction

The ReentrantLock and Synchronized functions are similar, more flexible and, of course, more manual.

We all know that synchronization is necessary only when competition for resources is involved.Write operations are naturally competition for resources, but read operations are not competition for resources.Simply put, a write operation can only have at most one thread (because a write operation involves data changes, multiple threads write at the same time, causing resource synchronization problems), and a read operation can have more than one (because no data changes are involved).

So ReentrantLock is a waste of resources in situations where you read more and write less.This requires a lock that distinguishes between read and write operations, ReentrantReadWriteLock.Read and write locks are available through ReentrantReadWriteLock.When a write lock exists, there can be only one thread holding the lock.When a write lock does not exist, multiple threads can hold it (write locks, which must wait until they are released before they can be held).

Lock and Condition Applications

        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        readLock.lock();
        readLock.unlock();

        readLock.newCondition();

        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        writeLock.lock();
        writeLock.unlock();

        writeLock.newCondition();

Unlike previous ReentrantLock applications, read locks need to be acquired through lock.readLock() and lock.writeLock(), then locked, unlocked, and Condition acquired.

2. Handwritten ReentrantLock

Get Requirements

At last we had a big meal.

First step, we need to decide what we want to do.

We're going to make a lock, let's just name it Jarry ReentrantLock.

This lock requires the following features: re-entrainable locks, pessimistic locks.

In addition, in order to be more specific and better integrated into AQS in the future, the lock needs to implement the Lock interface.

Lock's method signature, on the other hand, has been written since the beginning of the article, and will not be repeated here.

Of course, we're just demo here, so Condition s won't be implemented.Also, tryLock(long,TimeUnit) is no longer implemented, because when the whole is implemented, it is not as difficult as you might think.

Implementation principle of JarryReentrantLock

Now that the requirements have been determined, the API has also been determined.

Then the second step is to simply think about how to achieve it.

Class membership:

  1. First, we need an owner property to hold the thread object holding the lock.

  2. Second, since re-entrant locks are available, we need a count to hold the number of re-entries.

  3. Finally, we need a waiters property to hold threaded objects that are waiting (indefinitely) after a failed competitive lock.

Class method aspects:

  • tryLock: Attempts to acquire a lock, returns true successfully, and false failures.The first is the behavior of acquiring locks, either through CAS operations or, more simply, through Atomic packages (which are also CAS at the bottom).Also, since it is reentrant, when trying to acquire a lock, you need to determine if the thread trying to acquire the lock is the owner thread of the current lock.
  • Lock: Try to acquire the lock until it is successfully acquired.Seeing this spirit of success without success, my first thought was to call tryLock in a loop.But it's a waste of resources (long busy cycles are very CPU intensive after all).So manually suspend the current thread through LockSupport.park() and place it in the waiting queue waiters until the lock is released.
  • tryUnlock: Attempts to unlock, returns true successfully, and false failures.First, before releasing the lock, you need to determine if the thread you are trying to unlock is the same thread as the holder of the lock (thread A can't always release the lock held by thread B).Next, you need to determine whether counts is zero, the number of reentrants, to determine whether the holder of the lock is set to null.Finally, to avoid the count > 0, owner = null situation where other threads lock at the same time when count=0, count must be Atomic, and CAS operations must be used here (there are some difficulties to understand, you can see the code, there are relevant explanations).
  • Unlock: Unlock operation.An attempt to unlock is made here, and if the unlock succeeds, a thread needs to be awakened from the waiting queue waiters (the awakened thread continues to compete for the lock because it is in a loop).Keep in mind, however, that this thread does not necessarily compete for lock success, as there may be new threads, taking the first step.Then the thread will re-enter the queue.Therefore, only unfair locks are supported by JarryReentrantLock at this time.

Implementation of JarryReentrantLock

Next, code according to the previous information.

    package tech.jarry.learning.netease;
    
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.LockSupport;
    
    /**
     * @Description:  Imitate ReentrantLock to achieve its basic functions and features
     * @Author: jarry
     */
    public class JarryReentrantLock implements Lock {
    
        // Lock Counter
        private AtomicInteger count = new AtomicInteger(0);
        // Lock holder
        private AtomicReference<Thread> owner = new AtomicReference<>();
        // Waiting pool
        private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
    
    
        @Override
        public boolean tryLock() {
            // Determine if the current count is zero
            int countValue = count.get();
            if (countValue != 0){
                // countValue is not zero, meaning the lock is held by the thread
                // To determine if the owner of the lock is the current thread
                if (Thread.currentThread() == owner.get()){
                    // The lock is held by the current thread, then the lock is re-entered
                    // Now that the lock is occupied by the current thread, you don't have to worry about count s being modified by other threads, that is, you don't need to use CAS
                    count.set(countValue+1);
                    // Execute a reentrant lock, indicating that the current thread has acquired the lock
                    return true;
                }else{
                    // Returns false if the current thread is not the holder of the lock (this method is tryLock, that is, just try stopping)
                    return false;
                }
            }else {
                // countValue of 0 means that the current lock is not held by any thread
                // Modify count to 1 by CAS operation
                if (count.compareAndSet(countValue,countValue+1)){
                    // Successful count modification means that the thread acquired the lock (only one CAS successfully modified count, then the thread of the CAS is the lock holder)
                    // As to why you don't have to worry about visibility here, at first I was rather concerned about problems like reordering in doubleCheck (tryUnlock sets null)
                    // Looking at the source code below, the value in AtomicReference is volatile
                    owner.set(Thread.currentThread());
                    return true;
                } else {
                    // CAS operation failed, indicating that the current thread did not successfully modify count, i.e. acquisition of locks failed
                    return false;
                }
            }
        }
    
        @Override
        public void lock() {
            // lock() [endless] is equivalent to continually trying to acquire a lock after a failure to execute tryLock()
            if (!tryLock()){
                // After a failed attempt to acquire a lock, you can only enter the waiting queue waiers, wait for an opportunity, and continue tryLock()
                waiters.offer(Thread.currentThread());
    
                // Continuous attempts to acquire locks through spin
                // In fact, I didn't really understand why this was written at first to ensure that every thread executing lock() was always competing for locks.In fact, consider that all threads executing lock() have this loop.
                // Each unlock waits for the head of the queue to wake up (unpark), so the thread waiting for the head of the queue continues to try to acquire the lock, while the other threads of the queue continue to block (park)
                // This is why a series of operations, such as checking whether the current thread is waiting for a queue header element, need to be performed in the loop body.
                // In addition, threads in a waiting state may receive error alerts and pseudo-wakeups, and if the wait condition is not detected in the loop, the program will exit without meeting the end condition.In the end, whatever branch it is, it return s and ends the method.
                // Whole needs it even if there is no pseudo-wake problem because the thread needs to try to acquire the lock twice
                while (true){
                    // Gets the header element of the wait queue waiters (peek means get header element, but do not delete it).poll means to get the header element and delete its position in the queue)
                    Thread head = waiters.peek();
                    // If the current thread is the head element in the wait queue, the element that has just joined the current wait queue is indicated.
                    if (head == Thread.currentThread()){
                        // Try to acquire the lock again
                        if (!tryLock()){
                            // The attempt to acquire the lock again failed by suspending the thread (that is, the current thread),
                            LockSupport.park();
                        } else {
                            // Successfully acquired lock, removing the thread (the header element of the waiting queue) from the waiting queue waiters
                            waiters.poll();
                            return;
                        }
                    } else {
                        // If you wait for the head element of the queue, not the current thread, it means that there are other threads waiting for the queue before the current thread joins
                        LockSupport.park();
                    }
                }
            }
        }
    
        private boolean tryUnlock() {
            // First determine if the current thread is the lock holder
            if (Thread.currentThread() != owner.get()){
                // If the current thread is not the holder of the lock, an exception is thrown
                throw new IllegalMonitorStateException();
            } else {
                // If the current thread is the lock holder, count-1 first
                // In addition, unlocking can only be performed at the same time by the holder thread of the lock, so there is no need to worry about atomicity (atomicity is only meaningful if it is discussed in a multithreaded scenario)
                int countValue = count.get();
                int countNextValue = countValue - 1;
                count.compareAndSet(countValue,countNextValue);
                if (countNextValue == 0){
                    // If the current count is 0, it means that the lock holder has completely unlocked successfully and should therefore lose the lock holder (that is, set owner to null)
                    // In fact, I was very entangled at first. Why do I need CAS here?Only the current thread can walk to the program anyway.
                    // First, why use CAS.Since count has been set to 0, other threads can modify count and owner.So it's possible to set owner=otherThread to owner=null without CAS, and eventually get stuck completely
                    //TODO_FINISHED but unpark in unlock() is not executed, there are no other threads at all.Embarrassed
                    // The code here is also designed to reflect some of the features of the source code.The actual source code is to abstract these characteristics to a higher level to form an AQS.
                    // Although tryUnlock is implemented by an implementation subclass, countNextValue comes from countValue (which in JarryReadWriteLock is writeCount), and in AQS source is implemented by state
    
                    // Secondly, there is no ABA problem.Since ABA needs to modify the expect value of CAS to current Thread, the current thread can only execute on a single thread, so it will not.
                    // Finally, the operation where owner is set to null is unnecessary.The actual source may be needed, but it does not seem necessary here.
                    owner.compareAndSet(Thread.currentThread(),null);
                    // Unlock succeeded
                    return true;
                } else {
                    // count is not zero, unlock is not complete
                    return false;
                }
            }
        }
    
        @Override
        public void unlock() {
            if (tryUnlock()){
                // If the current thread succeeds in tryUnlock, the current lock is empty.Then you need to "put" one out of the waiters, oh
                Thread head = waiters.peek();
                // A simple judgment is needed to prevent waiters from throwing exceptions when they are empty
                if (head != null){
                    LockSupport.unpark(head);
                }
            }
        }
    
    
        // Non-core functionality is not implemented, at least not now.
    
        @Override
        public void lockInterruptibly() throws InterruptedException {
    
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return false;
        }
    
        @Override
        public Condition newCondition() {
            return null;
        }
    }

There are no explanations here.Because of the explanations needed, the notes are clear, including some pits I stepped on.

If there are still some things you can't understand or are wrong with, welcome @me or trust me privately.

Three, handwritten ReentrantReadWriteLock

Get Requirements

Like ReentrantLock, the first step is to determine what we want to do.

We want to make a lock, let's just name it JarryReadWriteLock.

This lock requires the following features: read and write locks, re-entrainable locks, pessimistic locks.

On the one hand, for better understanding (the first version focuses on understanding the basics, isn't it), and on the other hand, for better reuse of the code from the previous ReentrantLock (after all, ReentrantLock is actually a write lock for a read-write lock, isn't it). The API of JarryReadWriteLock here is no longer the same as the official ReentrantReadWriteLock, but has been slightly adjusted.Direct invocation of the associated read lock's unlocked API, already associated write lock's unlocked API.See the code section.

Implementation principle of JarryReadWriteLock

Now that the requirements have been determined, the API has also been determined.

Then the second step is to simply think about how to achieve it.

Class membership:

  1. First, we need an owner property to hold the thread object holding the write lock.

  2. Second, since write locks are re-entrant locks, we need a readCount to hold the number of re-entries.

  3. Then, since read locks can be held by multiple threads, we need a writeCount to hold the number of read lock holding threads.

  4. Finally, we need a waiters property to hold threaded objects that are waiting (indefinitely) after a failed competitive lock.

Custom data structure:

There is a question here.How to determine what type of lock a thread trying to acquire a lock wants to acquire.In the API call phase, we can judge by the API.But how do we decide when we put it in the waiting queue?If, as before, waiting for a queue is simply a thread object holding a competing lock, it is not enough.

So we need to create a new WaitNode lass to hold the thread objects in the waiting queue and the necessary information.So WaitNode has the following properties:

  • Thread thread: The thread that identifies the waiter.
  • int type: Identifies the type of lock that the thread object wants to compete against.0 is a write lock (exclusive lock) and 1 is a read lock (shared lock).
  • int arg: Extended parameter.In fact, the handwritten simplified version does not show any value.But Node in AQS is a similar design.However, in AQS, Node is not saved by queue, but by a chain table.

Class method aspects:

  • Exclusive locks:
    • tryLock: Similar to Jarry ReentrantLock, with two additional points.On the one hand, you need to consider whether the shared lock is occupied.On the other hand, the acquire parameter (currently a fixed value) needs to be introduced in response to WaitNode's arg.
    • lock: Similar to JarryReentrantLock, but arg needs to be set manually.
    • tryUnlock: Similar to JarryReentrantLock, the release parameter (currently a fixed value) needs to be introduced in response to WaitNode's arg.
    • unlock: Similar to JarryReentrantLock, but arg needs to be set manually.
  • Shared locks:
    • tryLockShared: An attempt to acquire a shared lock returned true successfully and false failed.It's similar to tryLock for exclusive locks, except that extra consideration needs to be given to whether exclusive locks already exist.In addition, to achieve lock demotion, if an exclusive lock exists, you need to determine whether the owner of the exclusive lock is consistent with the thread currently trying to acquire the shared lock.
    • lockShared: Get the shared lock until it succeeds.Since WaitNode.type already exists to determine the lock type, shared locks and exclusive locks use the same queue.Again, arg needs to be set manually here.Other aspects are essentially consistent with lock operations for exclusive locks.
    • tryUnlockShared: Attempt to release lock, success returns true, failure returns false.Similar to tryUnlock, except that the release parameter (fixed value) is added, which corresponds to the arg of WaitNode.
    • unlockShared: Release the lock.Like unlock, but arg needs to be set manually.

Implementation of JarryReentrantLock

    package tech.jarry.learning.netease;
    
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.locks.LockSupport;
    
    /**
     * @Description: 
     * @Author: jarry
     */
    public class JarryReadWriteLock {
    
        // Lock counters for reading locks (shared locks). It's really necessary to volatile here (volatile when value in Atomic). Look at the following code
        // There really is no need for volatile here, and as far as the source code is concerned, it implements readCount and writeCount through a bit operation on a variable state
        volatile AtomicInteger readCount = new AtomicInteger(0);
        // Lock counters for write locks (exclusive locks) volatile is not used here because only one thread is changing the writeCount (even if there is a cache, this thread, so there is no problem with the cache)
        AtomicInteger writeCount = new AtomicInteger(0);
        // The holder of the lock used to hold it (in this case, the lock holder of a write lock (exclusive lock)
        AtomicReference<Thread> owner = new AtomicReference<>();
        // Used to hold threads expecting locks (to distinguish the type of locks a thread expects to acquire, a new data type is created here (implemented through an internal class))
        public volatile LinkedBlockingQueue<WaitNode> waiters = new LinkedBlockingQueue<>();
    
        // Internal class implements custom data types in waiting queues
        class WaitNode{
            // Threads representing the waiter
            Thread thread = null;
            // Indicates the type of lock you want to strive for.0 for write lock (exclusive lock) and 1 for read lock (shared lock)
            int type = 0;
            // Parameter, acquire, state dependent, see again
            int arg = 0;
    
            public WaitNode(Thread thread, int type, int arg) {
                this.type = type;
                this.thread = thread;
                this.arg = arg;
            }
        }
    
        /**
         * Attempt to acquire an exclusive lock (for exclusive locks)
         * @param acquires Number of times to lock.Typically, waitNode.arg is passed in (1 in this code).Why don't you use a constant 1 and you don't know?(can better dock AQS)
         * @return
         */
        public boolean tryLock(int acquires){
            //TODO_FINISHED Here the readCount's judgment, and the operation to modify the writeCount, can be split, not atomic.It is not possible that both readCount and writeCount values are greater than zero.
            // This sample code does have this problem, but the actual source code, writeCount and readCount, are implemented through the same variable state, so CAS is a good way to ensure atomicity
    
            // readCount represents the number of times a read lock (shared lock) has been locked
            if (readCount.get() == 0){
                // A readCount value of 0 indicates that the read lock (shared lock) is empty, so it is possible for the current thread to acquire a write lock (exclusive lock).
                // Next, determine if the write lock (exclusive lock) is occupied
                int writeCountValue = writeCount.get();
                if (writeCountValue == 0){
                    // A write lock (exclusive lock) has a lock count of 0, indicating that the write lock (exclusive lock) is not held by any thread
                    if (writeCount.compareAndSet(writeCountValue,writeCountValue+acquires)){
                        // Modify the writeCount to obtain the lock.This mechanism is the same as ReentrantLock
                        // Set owner of exclusive lock
                        owner.set(Thread.currentThread());
                        // At this point, the current thread successfully grabbed the lock
                        return true;
                    }
                } else {
                    // The number of locks for a write lock (exclusive lock) is not zero, indicating that the write lock (exclusive lock) has been held by a thread
                    if (Thread.currentThread() == owner.get()){
                        // If the thread holding the lock is the current thread, a lock reentry operation is performed
                        writeCount.set(writeCountValue+acquires);
                        // Re-entry locks, indicating that the current thread holds locks
                        return true;
                    }
                    // The read lock is not occupied, but the write lock is occupied, and the thread occupying the write lock is not the current thread
                }
            }
            // Read lock occupied
            // Other cases (1. Read lock occupied, 2 read lock not occupied, but write lock occupied, and the thread occupying the write lock is not the current thread) all return false
            return false;
        }
    
        /**
         * Acquire exclusive locks (for exclusive locks)
         */
        public void lock(){
            // Setting the arg parameter in waitNote
            int arg = 1;
            // Attempt to acquire an exclusive lock.Success exits the method, failure enters the "never-ending" logic
            if (!tryLock(arg)){
                // The current thread needs to be saved to the waiting queue before it can be encapsulated as waitNote
                WaitNode waitNode = new WaitNode(Thread.currentThread(), 0, arg);
                // Place the encapsulated waitNode in the waiting queue waiters (the offer method returns false directly when the queue is full.put is blocking.add throws an exception)
                waiters.offer(waitNode);
    
                // As with ReentrantLock, start a loop to try to hold the lock
                while (true){
                    // Get Queue Header Element
                    WaitNode headNote = waiters.peek();
                    // If the headNote waiting for the queue header element is not null (could it be null?)And is the current thread, then try to acquire the lock
                    if (headNote !=null && headNote.thread == Thread.currentThread()){
                        // If another attempt to acquire the lock fails, it can only be suspended
                        if (!tryLock(headNote.arg)){
                            LockSupport.park();
                        } else {
                            // If another attempt to acquire the lock succeeds, kick the queue head element out of the waiting queue waiters
                            waiters.poll();
                            return;
                        }
                    }else {
                        // If headNote is not encapsulated by the current thread, it is suspended directly (headNote==null is not handled here)
                        LockSupport.park();
                    }
                }
            }
        }
    
        /**
         * Attempt to unlock (for exclusive locks)
         * @param releases Used to set the number of unlocks.General incoming waitNode.arg
         * @return
         */
        public boolean tryUnlock(int releases){
            // First determine if the lock holder is the current thread
            if (owner.get() != Thread.currentThread()){
                // The lock holder is not the current thread (even if the lock holder is null and the lock holder is null, an exception is thrown)
                throw new IllegalMonitorStateException();
            }
            // The lock holder is the current thread
            // First unlock by releases (after some thought, there won't be a problem like DoubleCheck (the value in Atomic is volatile), so only one thread will operate on it at the same time)
            int writeCountValue = writeCount.get();
            // Set a new value for writeCount
            writeCount.set(writeCountValue-releases);
            // Determine if the lock holder has changed based on the new value of writeCount
            if (writeCount.get() == 0){
                // The value of writeCount is 0, indicating that the current thread is fully unlocked, so the holder of the modified lock is null
                owner.set(null);
                // And that means complete unlocking was successful
                return true;
            } else {
                // The value of writeCount is not zero, indicating that the current thread has not been fully unlocked and that the holder of the lock has not changed.That is, the attempt to unlock failed
                return false;
            }
        }
    
        /**
         * Unlock (for exclusive locks)
         */
        public void unlock(){
            // Set the parameter releases for tryUnlock
            int arg = 1;
            // Try to unlock first
            if (tryUnlock(arg)){
                // Get the header element of the waiting queue
                WaitNode head = waiters.peek();
                // Check if the header element head null (there may be no elements at all in the waiting queue)
                if (head == null){
                    // If the header element head null, the queue is null, return directly
                    return;
                }
                // If the unlock succeeds, unpark the header element in the waiting queue
                // It is important to note that even if the header element of the queue is awakened, it is not necessarily the header element that gets the lock (see tryLock for details, new threads may get the lock)
                // If this header element cannot get a lock, it will be a park (while loop).And one park, you can unpark many times
                LockSupport.unpark(head.thread);
            }
        }
    
        /**
         * Attempt to acquire shared locks (for shared locks)
         * @param acquires
         * @return
         */
        public boolean tryLockShared(int acquires){
            // Determines whether a write lock (exclusive lock) is held by another thread (this condition means that the same thread can hold both read and write locks)
            // This method is for lock demotion****
            if (writeCount.get() == 0 || owner.get() == Thread.currentThread()){
                // If no other thread holds a write lock (exclusive lock), you can continue to attempt to acquire a read lock (shared lock)
                // Locking is achieved by spinning through loops (avoiding lock failure)
                while(true){
                    // Since read locks (shared locks) are shared and there is no exclusive behavior, increase the number of Acquis of current thread lock behavior directly at writeCount
                    int writeCountValue = writeCount.get();
                    // Increased number of shared locks through CAS
                    if (writeCount.compareAndSet(writeCountValue, writeCountValue+acquires)){
                        break;
                    }
                }
            }
            // Write lock already held by another thread, shared lock acquisition failed
            return false;
        }
    
        /**
         * Acquire shared locks (for shared locks)
         */
        public void lockShared(){
            // Setting the arg parameter in waitNote
            int arg = 1;
            // Determine if successful acquisition of shared locks
            if (!tryLockShared(arg)){
                // If acquiring a shared lock fails, enter the waiting queue
                // As with the synchronous lock acquisition operation, the current thread needs to be wrapped in WaitNote first
                WaitNode waitNode = new WaitNode(Thread.currentThread(),1,arg);
                // Place waitNote in waiters (the offer method returns false directly when the queue is full.put is blocking.add throws an exception)
                waiters.offer(waitNode);
    
                // Use loops.On the one hand, avoid false wake-up, on the other hand, facilitate the second attempt to acquire the lock
                while (true){
                    // Gets the head element of the waiting queue waiters
                    WaitNode head = waiters.peek();
                    // Verify that the header is null and determine if the header element of the waiting queue is encapsulated by the current thread (perhaps the header is encapsulated by the current thread, but it does not mean that the header is the element that just placed the waiters)
                    if (head != null && head.thread == Thread.currentThread()){
                        // If the check passes and waits for the head er element of the queue to be encapsulated by the current thread, try to acquire the lock again
                        if (tryLockShared(head.arg)){
                            // Successfully acquired shared lock removes the header element from the current queue (poll() method removes the queue header element)
                            waiters.poll();
    
                            // This is where unlike exclusive locks, which means that only one thread can acquire a lock, and shared locks can be acquired by multiple threads
                            // Get the new header element newHead for the waiting queue
                            WaitNode newHead = waiters.peek();
                            // Verify that the element is null and determine if its lock type is a shared lock
                            if (newHead != null && newHead.type == 1){
                                // If the waiting queue's new header element strives for a shared lock, wake it up (this is an iterative process in which the newly waked thread will do the same)
                                //TODO_FINISHED I'm a little confused here, so if the waiting queue is like this {shared locks, shared locks, exclusive locks, shared locks}, shared locks are separated by an exclusive lock.Can't you wake up the shared lock in the back?Take a look at the code behind
                                // This is not the actual source.The teacher said that the existing code is like this, do not need to understand so deeply, have the opportunity to see the source code in the future
                                LockSupport.unpark(newHead.thread);
                            }
                        } else {
                            // Suspend if acquiring the shared lock again fails
                            LockSupport.park();
                        }
                    } else {
                        // Suspend the current thread if the check fails or if the head er element of the waiting queue is not encapsulated by the current thread
                        LockSupport.park();
                    }
                }
            }
        }
    
        /**
         * Attempt to unlock (for shared locks)
         * @param releases
         * @return
         */
        public boolean tryUnlockShared(int releases){
            // Reduce the number of locks shared by the CAS operation, that is, the value of readCount (since shared locks are possible for multiple threads to reduce this value at the same time, CAS is used)
            while (true){
                // Get the value of a read lock (shared lock)
                int readCountValue = readCount.get();
                int readCountNext = readCountValue - releases;
                // Jump out only if the value is successfully modified
                if (readCount.compareAndSet(readCountValue,readCountNext)){
                    // Used to indicate that the shared lock was fully unlocked successfully
                    return readCountNext == 0;
                }
            }
            // Since there is no owner for the read lock, no owner operation is required
        }
    
        /**
         * Unlock (for shared locks)
         */
        public boolean unlockShared(){
            // Set parameter releases for tryUnlockShared
            int arg = 1;
            // Determine if an attempt to unlock was successful
            if (tryUnlockShared(arg)){
                // If the attempt to unlock succeeds, you need to wake up the thread waiting for the head er element of the queue
                WaitNode head = waiters.peek();
                // Verify that the head er is null, after all, the queue may be waiting for null
                if (head != null){
                    // Wake up the thread waiting for the head element of the queue
                    LockSupport.unpark(head.thread);
                }
                //When TODO_FINISHED successfully attempts to share a lock unlock, it should return true (although not quite understood)
                // For corresponding source code
                return true;
            }
            //After TODO_FINISHED's attempt to share a lock unlock fails, it should return false (although somewhat incomprehensible)
            // For corresponding source code
            return false;
        }
    }

There are no explanations.Because of the explanations needed, the notes are clear, including some pits I stepped on.

If there are still some things you can't understand or are wrong with, welcome @me or trust me privately.

Fourth, Summary

technology

  • CAS: Completes the competitive operation of locks by implementing atomic operations such as number of locks held by CAS.
  • Atomic: To simplify operations (avoid getting Unsafe, offset, etc.), CAS operations are implemented through Atomic.
  • Volatile: To avoid visibility issues under multithreading, the no cache feature of volatile is used.
  • transient: Avoid serialization of corresponding variables, as used in the source code.However, it was not used after consideration.
  • While: on the one hand, avoid pseudo-wake-up problems through while, on the other hand, drive the process through while (this requires looking at the code).
  • LinkedBlockingQueue: Implements a thread waiting queue.The actual AQS is a chain table structure made up of Node.
  • LockSupport: Implements thread suspension, wake-up and other operations through LockSupport.
  • IllegalMonitorStateException: An exception type that mimics Synchronized and at least looks clearer without having to implement a new Exception type yourself.

programme

In fact, these two demo s have two important aspects.On the one hand, you can feel for yourself how a lock is implemented and how it is designed.On the other hand, it's about thinking and designing about atomicity and visibility.

You can try to change something and then consider if there is a thread security issue after that change.Such considerations can be a huge boost to your online security.Anyway, I was changing that week.Even some of the changes could not be debugged at all, and then someone else was consulted to know some of the pits.Of course, some changes are possible.

an account of happenings after the event being told

If you have a problem, you can @me or trust me privately.

If you find this article good, click Recommendation.It's important for me, and for those who need it.

Thank you.

Posted by Inkybro on Mon, 25 Nov 2019 17:57:18 -0800