The article will be published on the public account, search attention: Different Java

Summary

  1. How to ensure thread safety
  2. Synchronization control in JDK concurrent package
  3. The characteristics of locks in JDK concurrent packages
  4. Why talk about AQS
  5. The core data structure of AQS
  6. How to apply for AQS exclusive lock
  7. How to release AQS exclusive lock

1. How to ensure thread safety

When Java multithreading accesses shared resources, if it is not controlled, there will be thread safety problems. When we use multithreading to access shared resources, we usually control the number of threads to access the shared resources:

  • Shared lock: We will assign permissions to shared resources, and only threads that get the permission can access the shared resources, otherwise you need to wait for the permission to be available (of course, you can also give up waiting here). The thread that has obtained the permission must return the permission when it leaves the shared interval (no longer accesses the shared resource), so that the waiting thread may get the permission to execute. (Semaphore uses this mechanism)
  • Exclusive lock: Only one thread can access the shared resource, and other threads can only wait outside the shared interval when the exclusive lock is not obtained. After the thread holding the lock releases the lock, other waiting threads may acquire the lock for execution

2. Synchronization control in JDK concurrent package

Java provides a large number of tools for us to perform multi-threaded access control on shared resources. Among them, the most familiar one is the synchronized keyword. In addition to this keyword, the JDK concurrency package also provides a large number of classes for synchronization control. Have:

  • ReentrantLock
  • Semaphore
  • ReadWriteLock
  • CountDownLatch
  • CyclicBarrier
  • LockSupport
  • Condition

3. The characteristics of the lock in the JDK package

3.1 ReentrantLock

The role of ReentrantLock is basically the same as that of synchronized, but ReentrantLock should require manual lock (wait if the lock is occupied) and unlock, and support thread interruption, support fair lock (acquire the lock in the order in which the lock is requested), try to obtain the lock You can specify the maximum waiting time (if you don't specify the time, the wait will not be performed, and the lock will return true, otherwise it will return false).

ReentrantLock with Condition's await() and signal() is equivalent to synchronized with Object.wait() and notify().

3.2 Semaphore

Semaphore (semaphore) allows multiple threads to access a shared resource at the same time. When constructing the semaphore, you must specify the maximum number of threads allowed. When using the semaphore, we will try to obtain a permit. If the acquisition fails, we need to wait until A thread release permits or the current thread is interrupted.

3.3 ReadWriteLock

ReadWriteLock (read-write lock) is a lock that separates reading and writing. There is no need for blocking mutual exclusion between reading and reading, but blocking mutual exclusion is required between reading and writing, writing and writing. A read lock and a write lock can be acquired in ReadWriteLock, and the concurrent operation efficiency of programs that read more and write less can be improved through the read-write lock.

3.4 CountDownLatch

CountDownLatch is a countdown counter. When constructing CountDownLatch, we need to pass a number. By calling the await method of CountDownLatch, we can block the current thread until the countdown counter is reduced to 0. Through the countdown() method of CountDownLatch, we can The value is subtracted by 1.

The function of CountDownLatch is like taking a plane. We have to wait for all passengers to check their tickets before the plane can take off.

3.4 CyclicBarrier

CyclicBarrier (CyclicBarrier), the role of the circular barrier is similar to CountDownLatch, but it will restore the original counter value after reducing the counter to 0. This is how the concept of loop comes from, and the circular barrier supports triggering after the counter is reduced to 0 An action (implementation class of Runnable interface).

3.5 LockSupport

LockSupport is a thread blocking tool. The mechanism is similar to a semaphore. A license can be consumed through park(). If there is no license currently, the thread will be blocked and unpark() releases a license. There is at most one license in LockSupport.

LockSupport will not cause a deadlock. Assuming that our thread is restricted to execute unpark first, and then call park, it will not block (because unpark has released a license that can be used).

If Thread.resume() is executed before the Thread.suspend() method, it will cause the thread to deadlock.

4. Why talk about AQS

WX20210328-113712@2x.png

From the above figure, we can see that the lock we mentioned earlier has a Sync class (in a combined way) inside, and this Sync class inherits from the AbstractQueuedSynchronizer class.

4.1 AQS core data structure

In AQS with two inner classes:

  • ConditionObject: holds the condition variable waiting queue (waiting caused by Condition.await()), and the final implementation is also Node
  • Node: The specific implementation class of the synchronous waiting queue (stored in the thread waiting on the lock)

4.2 Node

The data structure of Node is as follows:

  • volatile Node prev: waiting for the previous element in the linked list
  • volatile Node next: waiting for the next element in the linked list
  • volatile Thread thread: current thread object
  • Node nextWaiter: the next node waiting in the condition variable queue

In addition to the above 4 attributes, there is another important attribute:

  • volatile int waitStatus

This attribute represents the state of the node in the queue, and the int value of the state is in parentheses:

  • Initial state (0)
  • CANCELLED (1): The thread canceled the wait. If some exceptions occur during the lock acquisition process, the cancellation may occur, such as an interrupt exception or timeout during the waiting process.
  • SIGNAL (-1): indicates that the node needs to be awakened
  • CONDITION (-2): Indicates that the thread is waiting in the condition variable
  • PROPAGATE(-3)

4.3 ConditionObject

Since we only focus on the analysis of ordinary locks in this article and do not involve waiting for condition variables, readers and friends can read the source code by themselves.

5. Exclusive lock application

The following picture is a flowchart of ReentrantLock using exclusive locks:

WX20210403-162137@2x.png

Let's analyze the use process of exclusive locks. We can use ReentrantLock as the starting point to analyze the implementation of AQS:

public void lock() {
    sync.lock();
}

When we call the lock method of ReentrantLock, we will call the lock method () of Sync. Here, Sync is an abstract class. Here we mainly look at the implementation of non-fair locks, that is, the lock method of NonfairSync, as follows:

final void lock() {
    // 1
    if (compareAndSetState(0, 1))
    // 2
        setExclusiveOwnerThread(Thread.currentThread());
    else
    // 3
        acquire(1);
}
  1. Try to obtain the lock through CAS (change the state attribute value in AQS to 1), if the attempt to obtain the lock is successful, perform the second step, otherwise perform the third step
  2. After CAS acquires the lock successfully, set the thread holding the lock as the current thread
  3. If the lock is not acquired, call acquire to try to obtain permission, because here is an exclusive lock, and only one thread can hold the lock

5.1 acquire

The acquire method is a method in AQS, as follows:

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

First call tryAcquire to try to acquire the lock again. The implementation of this method in AQS throws UnsupportedOperationException, which means that the subclass must implement this method. Let's take a look at the implementation of tryAcquire in the ReentrantLock$NonfairSync method. This method will eventually Call the nonfairTryAcquire(int acquires) method in ReentrantLock$Sync as follows:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

The main logic here is to first acquire the state of the lock. If the state of the lock is unoccupied (state value is 0), CAS is used again to try to occupy the lock, and if the attempt is successful, it returns true.

After the failed attempt to acquire the lock through CAS, it is judged whether the lock is held by the current thread. If so, call setState(nextc) to add the value of state in AQS by +1, and then return true (this solves the reentrancy problem).

If neither of the above two conditions are true, false is returned, indicating that the current thread has failed to acquire the lock.

5.2 acquireQueued method

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

When we fail to acquire the lock, we will call acquireQueued to add the thread to the waiting queue. So how is the Node in this waiting queue constructed? The answer is the addWaiter method.

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;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

In the addWaiter method, a Node node is first constructed. The thread in the Node node is the current thread, and then tries to add the node to the waiting queue of AQS. If the tail node of the original waiting queue is not NULL, the CAS method of fast failure will be adopted. Add, if it is successfully added, return to this new node, otherwise call enq to add the new node to the waiting queue, the method is as follows:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            // 如果队列中还没有节点,CAS初始化一个空的头结点,并把尾节点设置为头结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // CAS设置当前节点node为尾节点,并把原来的尾节点的next指向当前节点node
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

If there is no node in the queue temporarily, initialize an empty head node and set the tail node as the head node, then change the previous node of the current node node to the tail node, and pass CAS until the node is successfully changed to tail node, and modify the next of the original tail node to the current node note, and then return the predecessor node of the current node node (that is, the original tail node).

When the node is added successfully, we will actively call acquireQueued to try to acquire the lock, the method is as follows:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 开启自旋
        for (;;) {
            // 获取当前节点node的前继节点
            final Node p = node.predecessor();
            // 如果前继节点是头结点(虚节点),那么意味着当前节点是第一个数据节点,那么久尝试获取锁,如果获取锁成功,将当前节点设置为头结点,setHead方法会将节点中的线程和prev节点都置为空
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果node的前继节点不是头结点或者获取锁失败,那么需要判断当前节点是否需要挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 获取如果发生异常,则取消获取锁申请
        if (failed)
            cancelAcquire(node);
    }
}

If it is judged whether the current node thread needs to be suspended, it mainly depends on shouldParkAfterFailedAcquire to judge, as follows:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

Through the above source code, we can see that only when the state of the current node's predecessor node is SINGAL (waiting to wake up), the current node thread can be suspended ( here is mainly to prevent infinite loop resource waste ). The above code also handles the special case that the predecessor node is cancelled, and modifies the predecessor node of the current node to the last node in the queue whose waitSatus is not in the canceled state.

The method to suspend the thread of the current node is as follows to implement the LockSupport used.

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

The method to cancel the acquisition application is cancelAcquire, as follows:

private void cancelAcquire(Node node) {
    // 过滤无效节点
    if (node == null)
        return;

    node.thread = null;

    // 跳过所有处于取消的节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // 找到最后一个有效节点的后继节点
    Node predNext = pred.next;

    // 将当前节点的状态设置为取消
    node.waitStatus = Node.CANCELLED;

    // 如果当前节点是尾节点,设置尾节点为最后一个有效节点
    if (node == tail && compareAndSetTail(node, pred)) {
        // 将最后一个有效节点的next设置为null
        compareAndSetNext(pred, predNext, null);
    } else {
        // 如果最后一个有效节点不是头结点并且(最后一个有效节点是SINGAL状态或者可以被设置为SGINAL状态),并且最后一个有效节点的线程不为null
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            // 如果当前节点后的后置节点是有效节点(存在且状态不为取消状态),设置最后一个有效节点的next为当前节点的后继节点
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            // 上述条件如果均不成立,唤醒当前节点的后继节点
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}

6. Exclusive lock release

public void unlock() {
    sync.release(1);
}

ReentrantLock is unlocked through the unlock method, which will call the release method in AQS, as follows:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease is implemented by a specific subclass of AQS. Let's take a look at the implementation in Sync in ReentrantLock, as follows:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 只有持有锁的线程才可以执行unlock操作
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 解锁成功,设置独占线程为空
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

Let's return to the release method of AQS, as follows:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

If the unlocking is successful, and the current node state is not the initial state, then call the unparkSuccessor method to wake up the thread of the successor node of the head, the method is as follows:

private void unparkSuccessor(Node node) {

    // 获取当前节点状态
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 获取节点的后继节点
    Node s = node.next;
    // 如果节点的后继节点为null或者已经被取消,则从尾节点找到一个未取消的节点进行唤醒
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 唤醒线程
        LockSupport.unpark(s.thread);
}

The unlocking process is relatively simple. When a thread releases ReentrantLock, it needs to wake up other nodes that need to be awakened from the waiting queue.

This is the introduction of Java AQS in this issue. I am shysh95. See you in the next issue!


shysh
82 声望17 粉丝