An overview,

This article is based on openJDK8u source code. Before reading this article, you need to understand concurrency.

In concurrency, to solve the problem of preemption of resources by multiple processes and threads in a program, the concept of locking was introduced in Java.

A variety of locks, for the first touch Java concurrent students, in the face of as many as 20 kinds of locks, instant meng force, back the game this chicken labor do not eat……

In fact, don’t worry, although there are many types of locks, but are derived from the concept of their features, if you are not very clear about Java locks, I hope this article can help you. Friends, if you can’t cook or don’t know what to eat, please follow me. Bah, learn Java well, refuse to indulge in a certain sound, ha ha ha ha ha ~

Below, is a mind map about the lock, with a general understanding, with this map edible effect better ha.

Ps: If you can’t see clearly, you can click on the original picture to view it.

Second,synchronized

For the sake of convenience, we can start from the shallow to the deep (suspected author driving but no evidence…). Let’s start with synchronized. For Java users, the synchronized keyword is an important way to implement locks.

package com.aysaml.demo.test;


import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/** * SynchronizedDemo example **@author cd
 * @dateThe 2019-11-26 * /
public class SynchronizedDemo {

    private static int count = 0;


    private static void addCount(a) {
        count++;
    }

    public static void main(String[] args) {

        int loopCount = 1000;

        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("demo-pool-%d").build();
        ExecutorService executorService = new ThreadPoolExecutor(10.1000.60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
        
        for (int i = 0; i < loopCount; i++) { Runnable r = SynchronizedDemo::addCount; executorService.execute(r); } executorService.shutdown(); System.out.println(SynchronizedDemo.count); }}Copy the code

Unguarded code paste, ha ha ha.

The above is a classic example of thread concurrency. Run this code and get a variety of results, 996, 997, 998….. The result is not 1000 as we expected, but synchronized is our first reaction to multithreaded resource competition.

private static synchronized void addCount(a) {
        count++;
    }
Copy the code

Adding synchronized to the cumulative method locks the method, so that the result of each execution is as expected. Java provides a built-in decompile tool javap to decompile bytecode files. You can run the javap -verbose -p synchronizeddemo. class command to view the bytecode files of this class.

A search for the locked method addCount() shows that synchronized modifiers use the ACC_SYNCHRONIZED token to specify that the method is a synchronized method and thus perform the corresponding synchronized call.

Also by looking at bytecodes, you can see that synchronized modifiers are addressed through monitorenter and Monitorexit directives, where monitorenter points to the start of the synchronized block. The Monitorexit directive indicates where the synchronized code block ends.

Three, all kinds of lock explanation, wretched development pick up guns and bullets

This section is just a brief introduction to the various locks and related concepts to give you a brief overview, and the corresponding implementation in Java will be introduced in a longer length.

Bias lock, lightweight lock, heavyweight lock

On the first time a program executes a synchronized block, the lock object becomes biased, that is, biased toward the first thread to acquire it. On the second execution of the program to change the block, the thread determines whether the thread holding the lock is itself, and if so, continues. Note that the bias lock is not released after the first execution of the synchronized block. From the point of view of efficiency, if the thread that executes the synchronized code block the second time is always the same, there is no need to re-lock, no extra overhead, high efficiency.

If only one thread executes code blocks synchronously, this is ideal. Once a second thread enters the lock contention, the biased lock is automatically upgraded to a lightweight lock. When the second thread wants to acquire the lock, and the lock is biased, it will determine whether the current thread holding the lock is still alive. If the thread holding the lock is not alive, then the biased lock will not be upgraded to a lightweight lock. What is lock contention: when one thread wants to acquire a lock held by another thread.

In this state, each thread continues to compete for the lock, and the thread that does not grab the lock circulates to judge whether it can successfully acquire the lock. This state is called spin, so lightweight lock is a kind of spin lock. The virtual machine has a counter to record the number of spins. By default, 10 spins are allowed. This value can be modified with the virtual machine parameter -xx: PreBlockSpin. If lock contention is particularly severe and this maximum number of spins is reached, lightweight locks will be upgraded to heavyweight locks. When another thread tries to acquire the lock and finds that the current lock is a heavyweight lock, it suspends itself and waits to be awakened in the future.

For more information on this, see Synchronized and the Three Locking States.

Fair lock, unfair lock

When the lock held by one thread is released, the other threads get the lock first in order, then the lock is a fair lock. On the other hand, if it is possible that the later thread will acquire the lock first, it is an unfair lock.

ReentrantLock in Java can specify whether it is a fair lock through its constructor. The default is unfair. Generally speaking, using unfair locks can achieve greater throughput, so it is recommended to use unfair locks in preference.

Synchronized is an unfair lock.

Optimistic lock, pessimistic lock

First, pessimistic locking, that is, when reading data, it always thinks that other threads will modify the data, so it takes the form of locking. Once this thread wants to read data, it locks the data, and other threads are blocked, waiting for the release of the lock. So pessimistic locking is summed up as pessimistic locking blocking threads.

When reading data, it always thinks that other threads will not modify the data. When updating data, it will judge whether other threads have updated the data. If there is an update, it will read again and try to update again.

In this way, optimistic locking actually has no lock, but only a comparison exchange method to ensure data synchronization, summed up as optimistic lockless rollback retry.

CAS (Comparison and Exchange)

CAS: Compare and swap. Optimistic lock also said above, in fact, is a process of comparison and exchange.

In A nutshell: Read A value of A, check if it is equal to A (compare), update A to B (swap), and do nothing else before updating the value to B.

In this way, resources can be synchronized between multiple threads without having to use locks. Obviously, without blocking threads, throughput can be greatly improved. The approach is good, but there are problems.

  • ABA problem, that is, if A value changes from A to B and back to A, then CAS will assume that the value has not changed.
    • A solution to this problem is to use the version number, which is used every time a variable is updatedThe version numberall+ 1, that is, byA->B->ABecomes a1A->2B->3A
  • Long cycle time and high cost. If lock competition is fierce, CAS will be repeatedly executed, consuming CPU resources.
  • The synchronization of only one variable is guaranteed, and obviously, because of its nature, CAS can only guarantee atomic operations on one shared variable.

Reentrant lock

A reentrant lock allows multiple threads to acquire the same lock multiple times, which means that the lock itself can be re-entered. For example, if there is a recursive function with a lock operation, if the lock does not block itself, it is a reentrant lock, so it is also called a recursive lock.

Synchronized locks are reentrant. Synchronized locks are reentrant. Synchronized locks are reentrant. Readers who are interested can learn how to implement a non-reentrant lock, but here is the definition of a lock.

Interruptible lock

If thread A holds the lock, thread B waits to acquire it. Because thread A has held the lock too long and thread B doesn’t want to wait any longer, we can have thread B interrupt itself or interrupt it in another thread, which is called A breakable lock.

In Java, synchronized is an uninterruptible Lock, and the classes that implement Lock are interruptible locks.

Exclusive lock, shared lock

Exclusive locks, also known as mutex locks, are easily understood to be held by only one thread at a time. Otherwise, it is shared lock.

Read lock, write lock

The above mentioned exclusive lock and shared lock, in fact, read and write lock is its most typical lock. Write locks are exclusive locks and read locks are shared locks. We’ll focus on the read-write lock implementation in Java later.

C, CAS in Java implementation, take this M4-CAS

From the above brief introduction to CAS, we believe that you have a relatively simple concept of CAS: single variable thread-safe through comparison and exchange.

JDK implementation of CAS in Java. The util. Concurrent. The atomic package:

The name of the class describe
AtomicBoolean It can be updated atomicallybooleanValue.
AtomicInteger It can be updated atomicallyintValue.
AtomicIntegerArray That can update its elements atomicallyintThe array.
AtomicIntegerFieldUpdater Reflection – based utility that can specify classesvolatile intField for atomic update.
AtomicLong It can be updated atomicallylongValue.
AtomicLongArray That can update its elements atomicallylongThe array.
AtomicLongFieldUpdater Reflection – based utility that can specify classesvolatile longField for atomic update.
AtomicMarkableReference AtomicMarkableReferenceMaintains object references with marker bits that can be updated atomically. It is used to solve THE ABA problem, and it only cares if it has been modified.
AtomicReference An object reference that can be updated atomically.
AtomicReferenceArray An array of object references whose elements can be updated atomically.
AtomicReferenceFieldUpdater Reflection – based utility that can specify classesvolatileField for atomic update.
AtomicStampedReference AtomicStampedReferenceMaintains object references with integer “flags” that can be updated atomically. To solve THE ABA problem, with the aboveAtomicMarkableReferenceIn contrast, it is not only whether the relationship has been modified, but also how many times it has been modified.

Take AtomicInteger as an example to see how it can ensure thread-safe operations on int.

package java.util.concurrent.atomic;
import java.util.function.IntUnaryOperator;
import java.util.function.IntBinaryOperator;
import sun.misc.Unsafe;

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

    // Use Unsafe.compareAndSwapInt to update data
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // Memory offset, that is, memory address
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw newError(ex); }}private volatile int value;

    /** * constructor ** with initial value@param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /** * The no-argument constructor defaults to 0 */
    public AtomicInteger(a) {}/** * gets the current value **@return the current value
     */
    public final int get(a) {
        return value;
    }

    /** * Sets the given value. Since value is volatile, other threads will see the change immediately@param newValue the new value
     */
    public final void set(int newValue) {
        value = newValue;
    }

    /** * Sets the given value. Calling the Unsafe delay setting method does not guarantee that the result will be immediately seen by other threads@param newValue the new value
     * @since1.6 * /
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }

    /** * Atomic set new value, return old value **@param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    /** * Set the new value using CAS, return true ** on success@param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    /** * Use CAS to update the value ** <p><a href="package-summary.html#weakCompareAndSet">May fail * spurspur and does not provide ordering guarantees</a>, so is * only rarely an appropriate alternative to {@code compareAndSet}.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful
     */
    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    /** * atom increases **@return the previous value
     */
    public final int getAndIncrement(a) {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    /** ** atom minus 1 **@return the previous value
     */
    public final int getAndDecrement(a) {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }

    /** * atom increments the given value **@param delta the value to add
     * @return the previous value
     */
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet(a) {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

    /**
     * Atomically decrements by one the current value.
     *
     * @return the updated value
     */
    public final int decrementAndGet(a) {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }

    /**
     * Atomically adds the given value to the current value.
     *
     * @param delta the value to add
     * @return the updated value
     */
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    /**
     * Atomically updates the current value with the results of
     * applying the given function, returning the previous value. The
     * function should be side-effect-free, since it may be re-applied
     * when attempted updates fail due to contention among threads.
     *
     * @param updateFunction a side-effect-free function
     * @return the previous value
     * @since1.8 * /
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while(! compareAndSet(prev, next));return prev;
    }

    /**
     * Atomically updates the current value with the results of
     * applying the given function, returning the updated value. The
     * function should be side-effect-free, since it may be re-applied
     * when attempted updates fail due to contention among threads.
     *
     * @param updateFunction a side-effect-free function
     * @return the updated value
     * @since1.8 * /
    public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while(! compareAndSet(prev, next));return next;
    }

    /** * Atomically updates the current value with the results of * applying the given function to the current and given values, * returning the previous value. The function should be * side-effect-free, since it may be re-applied when attempted * updates fail due to contention among threads. The function * is applied with  the current value as its first argument, * and the given update as the second argument. * *@param x the update value
     * @param accumulatorFunction a side-effect-free function of two arguments
     * @return the previous value
     * @since1.8 * /
    public final int getAndAccumulate(int x,
                                      IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while(! compareAndSet(prev, next));return prev;
    }

    /** * Atomically updates the current value with the results of * applying the given function to the current and given values, * returning the updated value. The function should be * side-effect-free, since it may be re-applied when attempted * updates fail due to contention among threads. The function * is applied with  the current value as its first argument, * and the given update as the second argument. * *@param x the update value
     * @param accumulatorFunction a side-effect-free function of two arguments
     * @return the updated value
     * @since1.8 * /
    public final int accumulateAndGet(int x,
                                      IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while(! compareAndSet(prev, next));return next;
    }

    /**
     * Returns the String representation of the current value.
     * @return the String representation of the current value
     */
    public String toString(a) {
        return Integer.toString(get());
    }

    /**
     * Returns the value of this {@code AtomicInteger} as an {@code int}.
     */
    public int intValue(a) {
        return get();
    }

    /**
     * Returns the value of this {@code AtomicInteger} as a {@code long}
     * after a widening primitive conversion.
     * @jls10 experiments for Primitive Conversions */
    public long longValue(a) {
        return (long)get();
    }

    /**
     * Returns the value of this {@code AtomicInteger} as a {@code float}
     * after a widening primitive conversion.
     * @jls10 experiments for Primitive Conversions */
    public float floatValue(a) {
        return (float)get();
    }

    /**
     * Returns the value of this {@code AtomicInteger} as a {@code double}
     * after a widening primitive conversion.
     * @jls10 experiments for Primitive Conversions */
    public double doubleValue(a) {
        return (double)get(); }}Copy the code

The source notes write very detailed, write notes half decided not to do Google translation… Atomic CAS operations are primarily implemented through methods such as compareAndSwapInt(Object Var1, Long Var2, int Var4, int VAR5), which Unsafe offers ways to perform low-level, Unsafe operations, For example, you can directly access and manage system memory resources.

The CAS operation contains three operands – the memory location, the expected old value, and the new value. When the CAS operation is performed, the value of the memory location is compared to the expected value. If it matches, the processor automatically updates the value of the memory location to the new value; otherwise, the processor does nothing. Addressing Unsafe, CAS is an atomic CPU instruction (CMPXCHG) that doesn’t cause data inconsistencies. Its underlying implementation of CAS methods such as compareAndSwapXXX is the CPU instruction CMPXCHG.

For those interested, check out the “Java Magic Classes: Unsafe” application Parsing.

Four, AQS to put on your class A

AbstractQueuedSynchronizer AQS, full name, literal translation as abstract queue synchronizer, is the foundation of building lock or other synchronous component framework, can solve most of the synchronization problems. The realization principle can be simply understood as: synchronization state (STATE) + FIFO thread waiting queue.

  • AQS uses a member variable of type int state to indicate synchronization state. It uses the volatile keyword to ensure visibility between threads. A lock has been acquired when state > 0, and a lock has been released when state = 0. It provides three methods (getState(), setState(int newState), and compareAndSetState(int expect,int Update)) to operate on the synchronization state and ensure that operations on state are safe. State has different values for different locks:

    • In an exclusive lock, state =0 means that the lock is released, and state = 1 means that the lock is acquired.
    • In shared locks, state is the number of locks held.
    • Reentrant lock state is the number of reentrants.
    • Read/write locks are special. Since state is a variable of int type and is 32 bits, the middle is cut. The high 16 bits identify the number of read locks, and the low 16 bits identify the number of write locks.
  • FIFO thread wait queue Queues can be implemented using arrays or nodes. AQS uses Node to implement queues.

static final class Node {
        /** indicates that a node is in shared mode. The default is shared mode */
        static final Node SHARED = new Node();
        /** marks a node as in exclusive mode */
        static final Node EXCLUSIVE = null;

        
        static final int CANCELLED =  1;
      
        static final int SIGNAL    = -1;
        
        static final int CONDITION = -2;
       
        static final int PROPAGATE = -3;

        /** This variable represents the state of the current thread */
        volatile int waitStatus;

	Precursor / * * * /
        volatile Node prev;

	/ * * * / be carried forward
        volatile Node next;

	/** is used to save threads */
        volatile Thread thread;
	
	/** Save the next Node in the waiting state */
        Node nextWaiter;

        /** * is used to check whether the mode is shared */
        final boolean isShared(a) {
            return nextWaiter == SHARED;
        }

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
        final Node predecessor(a) throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // No parameter constructor, default is shared mode
        }

        Node(Thread thread, Node mode) {     // Used to construct the next waiting thread Node Node
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used to construct a Node with the state of the thread
            this.waitStatus = waitStatus;
            this.thread = thread; }}Copy the code

Two nodes are defined in AQS, which are head and tail nodes:

/** The head of the queue, which is the initialization Node of the queue, can only be set using the setHead() method, which emptens Node variables for timely GC. While it has a value, waiteStatus must be CANCELLED. * /
    private transient volatile Node head;

    /** * is used to hold the end of the thread wait queue. * Set the value through the enq() method. * /
    private transient volatile Node tail;
Copy the code

The structure of this queue is shown below:

AQS a bunch of methods, according to the dimension of access lock and unlock can be divided into the following:

Gets a lock-related method

methods describe
acquire(int arg) Exclusive mode acquires locks, ignoring interrupts.
acquireInterruptibly(int arg) Exclusive mode acquires the lock and aborts it if interrupted.
acquireShared(int arg) Locks are acquired in shared mode, interrupts ignored.
acquireSharedInterruptibly(int arg) Shared mode acquires the lock and aborts it if it is interrupted.
tryAcquire(int arg) Try to acquire the lock in exclusive mode.Implemented by subclasses themselves.
tryAcquireNanos(int arg, long nanosTimeout) Attempts to acquire the lock in exclusive mode are aborted if interrupted, and nanosTimeout fails if a given timeout is reached.
tryAcquireShared(int arg) Try to get the lock in shared mode.
tryAcquireSharedNanos(int arg, long nanosTimeout) Attempts to acquire locks in shared mode are aborted if interrupted, and nanosTimeout fails if a given timeout is reached.
addWaiter(Node mode) Adds the current thread to the end of the CLH queue.
acquireQueued(final Node node, int arg) The current thread blocks and waits on fairness until the lock is acquired. And returns whether the current thread has been interrupted while waiting.
selfInterrupt() Generate an interrupt.

Unlocking methods

methods describe
release(int arg) Release objects in exclusive mode.
releaseShared(int arg) Release objects in shared mode.
tryRelease(int arg) An attempt was made to set the state to reflect a release in exclusive mode.Implemented by subclasses themselves.
tryReleaseShared(int arg) An attempt was made to set the state to reflect a release in shared mode.
unparkSuccessor(Node node) Used to wake up the node.

Five,LockTake you to eat chicken

In addition to synchronized, there are a number of implementations of the Lock interface to Lock in Java.

WriteLock, ReadLock, and ReentrantLock all implement Lock interfaces, corresponding to read locks, write locks, and ReentrantLock respectively. ReadWriteLock defines read locks and write locks. ReentrantReadWriteLock implements read and write locks in the form of static internal classes.

First create a singleton read/write lock:

package com.aysaml.demo.test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/** * singleton read/write lock **@author wangning
 * @dateThe 2019-11-26 * /
public enum Locker {

    instance;

    private Locker(a) {}private static final ReadWriteLock lock = new ReentrantReadWriteLock();

    public Lock writeLock(a) {
        returnlock.writeLock(); }}Copy the code

This can be done by placing a lock on the above summation method:

private static void addCount() {
        Lock locker = Locker.instance.writeLock();
        locker.lock();
        count++;
        locker.unlock();
    }
Copy the code

Look at the results of the operation, as desired (always feel this word used in the wrong, really really can not think of what word, ha ha, meaning you understand good).

Comparison of two locking methods:

Synchronized is a mutex. Only one thread is allowed to read and write at any time, and other threads must wait. ReadWriteLock allows multiple threads to acquire read locks, but only one thread to acquire write locks, which is relatively efficient.

Looking at the implementation diagram of the Lock interface above, we know that there are three important implementations of locking in Java, and we’ll look at each one below.

Read lock Write lock Abstract queue synchronizer level 3 A


Read/write locks are shared locks that allow multiple threads to acquire locks at the same time. In some services, read operations are more than write operations. Compared with synchronized, read/write locks use CAS to ensure resource synchronization. Therefore, read/write locks greatly increase throughput.

Java ReentrantReadWriteLock implements read/write locks in the form of internal classes as follows:

Then look at their implementation separately:

  • ReadLock

  • WriteLock

You can see that both lock operations have one key thing Sync:

Sync is ReentrantReadWriteLock inside an abstract class, it inherits the AbstractQueuedSynchronizer and realize the sharing and exclusive way of synchronous operation. Read/write locks are a pair of shared and exclusive locks, while synchronized queues also have shared and exclusive locks, so we can see the workflow of AQS from their locking and unlocking respectively:

Write lock (exclusive)

lock

public void lock(a) {
            sync.acquire(1);
        }
Copy the code

This is WriteLock’s acquire(int arg) method, which is implemented in AQS and is not overwritten in subclasses.

public final void acquire(int arg) {
        if(! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }Copy the code

Acquire (int arg) acquires (int arg)

  • TryAcquire: Attempts to acquire a lock, set the lock status and return true on success, false otherwise. Subclasses are required to implement their own.
  • AddWaiter: Adds the current thread to the end of the CLH queue. Already implemented.
  • AcquireQueued: The current thread blocks and waits in fairness until the lock is acquired. And returns whether the current thread has been interrupted while waiting. Already implemented.
  • SelfInterrupt: Generates an interrupt. Already implemented.

Previously, we said that AQS consists of thread state and thread wait queue. The process of AQS locking and unlocking is actually the modification of thread state and the operation of waiting queue entering and leaving queue. Subclasses of AQS can override tryAcquire(int Acquires) to modify state. Sync in ReentrantReadWriteLock overrides the tryAcquire method:

protected final boolean tryAcquire(int acquires) {
            // Get the current thread
            Thread current = Thread.currentThread();
	    // Get the state variable, which is the number of locks
            int c = getState();
	    // Obtain the number of write locks. The lower 16 bits represent the number of write locks
            int w = exclusiveCount(c);
	    // If the thread already holds the lock
            if(c ! =0) {
                // (Note: if c ! = 0 and w == 0 then shared count ! = 0)
		// If the number of write locks is 0 or the thread holding the lock is not the current thread, return false
                if (w == 0|| current ! = getExclusiveOwnerThread())return false;
		// If the number of write locks is greater than the maximum number (65535), an exception occurs
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
	    // If the number of writer threads is 0 and the current thread needs to block, failure is returned; Alternatively, failure is returned if the number of writer threads fails to be increased by CAS.
            if(writerShouldBlock() || ! compareAndSetState(c, c + acquires))return false;
	    // Set the current thread as the owner of the lock
            setExclusiveOwnerThread(current);
            return true;
        }
Copy the code

Consider the addWaiter method again:

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if(pred ! =null) {
            node.prev = pred;
	    // Set the tail through CAS
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                returnnode; }}// If not, try again
        enq(node);
        return node;
    }
Copy the code

Enq (Node Node)

private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; }}}}Copy the code

You can see that the ENQ method uses an endless loop to consistently try to set the endpoints until they succeed.

Tail points to the new node, the prev of the new node points to the last node, and the next of the last node points to the current node.

unlock

public void unlock(a) {
            sync.release(1);
        }
Copy the code

Unlocking calls Sync’s release method, and here’s what it does:

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h ! = null && h.waitStatus ! If (0) // Wake up the unparkprecursor (h); return true; } return false; }Copy the code

Like locking, tryRelease(ARG) is overridden by a subclass of AQS,

protected final boolean tryRelease(int releases) {
            if(! isHeldExclusively())throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
Copy the code

Releasing the lock is relatively simple, ** releasing the lock is done by changing the CAS waitStatus to 0 and then waking up the thread ** with locksupport.unpark (s.read).

To make it easier to understand, here’s a summary of WriteLock’s workflow flowchart:

Lovetolock operation

Read lock (shared)

The lock operation of read lock is similar to that of write lock:

public void lock(a) {
            sync.acquireShared(1);
        }
Copy the code

Call the custom tryAcquireShared(ARG) method to get the synchronization state

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
Copy the code

If this fails, call the doAcquireShared(int arg) method to get the synchronization status:

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return; }}if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true; }}finally {
            if(failed) cancelAcquire(node); }}Copy the code

TryAcquireShared (int unused)

protected final int tryAcquireShared(int unused) {
        
            Thread current = Thread.currentThread();
            int c = getState();
	    // If another thread has acquired the write lock, the current thread fails to acquire the read lock and enters the wait state
            if(exclusiveCount(c) ! =0&& getExclusiveOwnerThread() ! = current)return -1;
            int r = sharedCount(c);
            if(! readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null|| rh.tid ! = getThreadId(current)) cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }
Copy the code

In this method, you know that if another thread has acquired the write lock, the current thread fails to acquire the read lock and enters the wait state. If the current thread obtains the write lock or the write lock is not acquired, the current thread adds the read state and successfully obtains the read lock.


This article is from: aysaml.com/articles/20…