Briefly understand the relationship between locks and synchronizers

Custom exclusive lock

package com.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * Created by cxx on 2018/1/16.
 *
 * 独占锁示例
 */
public class Mutex implements Lock {

    //静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer{

        //是否处于占用状态
        protected boolean isHeldExclusively(){
            return getState() == 1;
        }

        //当状态为0的时候,获取锁
        public boolean tryAcquire(int acquires){
            if (compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        //释放锁,将状态设置为0
        public boolean tryRelease(int releases){
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        //返回一个condition,每个condition包含了一个condition队列
        Condition newCondition(){
            return new ConditionObject();
        }
    }

    /***
     * 将操作代理到Sync上即可
     */
    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);

    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);

    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public boolean isLocked(){
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads(){
        return sync.hasQueuedThreads();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);

    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

As shown in the code, the exclusive lock realizes that only one thread can acquire the lock at the same time, and other threads that acquire the lock can only wait in the synchronous waiting queue. Only after the thread acquiring the lock releases the lock, the subsequent threads can Acquire the lock.

#lock, synchronizer, user

  • Locks are user-oriented and define the interface for the user to interact with the lock, hiding the implementation details.
  • The synchronizer is oriented to the implementation of locks, which simplifies the implementation of locks and shields the underlying operations such as synchronization state management, thread queues, waiting and wake-up.
  • Locks and synchronizers are a good way to isolate the user from the areas that the implementation needs to focus on.

Like the mutex lock above, the specific implementation is delegated to sync, and the mutex only needs to define the interaction mode to the user.

AQS's approach

public method:

Acquisition and release of exclusive locks (including operations on synchronized queues)

  • acquire(int arg): exclusively acquires the synchronization state. If the current thread successfully acquires the synchronization state, it will be returned by this method. Otherwise, it will enter the synchronization queue and wait. This method will call the overridable tryAcquire(int arg) method. ;

  • release(int arg): Exclusively releases the synchronization state, this method will wake up the thread contained in the first node in the synchronization queue after releasing the synchronization state;

Acquisition and release of shared locks (including operations on synchronized queues)

  • acquireShared(int arg): The shared acquisition synchronization state. If the current thread does not acquire the synchronization state, it will enter the synchronization queue to wait. The main difference from the exclusive type is that multiple threads can acquire the synchronization state at the same time;
  • releaseShared(int arg): shared release synchronization state;

Protected methods that need to be implemented by subclasses

The specific implementation of the acquisition and release of exclusive locks (there is no operation on the synchronization queue, the function is single)

  • tryAcquire(int arg): Obtain the synchronization state exclusively. After obtaining the synchronization state successfully, other threads need to wait for the thread to release the synchronization state to obtain the synchronization state;
  • tryRelease(int arg): exclusive release synchronization state;

Acquisition and release of shared locks (no operations on synchronized queues)

  • tryAcquireShared(int arg): shared acquisition synchronization status, the return value is greater than or equal to 0, it means the acquisition is successful, otherwise the acquisition fails;
  • tryReleaseShared(int arg): Shared release synchronization state;

Status of the operation queue synchronizer

  • getState(): Returns the current value of the synchronization state;
  • setState(int newState): Set the current synchronization state;
  • compareAndSetState(int expect, int update): Use CAS to set the current state, this method can ensure the atomicity of state settings;

other methods

  • isHeldExclusively(): Whether the current synchronizer is occupied by a thread in exclusive mode, generally this method indicates whether it is exclusively occupied by the current thread;
  • acquireInterruptibly(int arg): same as acquire(int arg), but this method responds to interruption, and the current thread enters the synchronization queue to acquire the synchronization state. If the current thread is interrupted, this method will throw InterruptedException and return ;
  • tryAcquireNanos(int arg, long nanos): The synchronization state is acquired over time. If the current thread does not acquire the synchronization state within nanos time, it will return false, and it will return true if it has been acquired;
  • acquireSharedInterruptibly(int arg): shared acquisition synchronization state, responding to interruption;
  • tryAcquireSharedNanos(int arg, long nanosTimeout): shared acquisition synchronization state, increase the timeout limit;

All methods of AQS are shown in Fig.

Enter image description

Enter image description

Enter image description

AQS summary

  • In the synchronizer built based on AQS, blocking can only occur at one moment, thereby reducing the overhead of context switching and improving the throughput. At the same time, the scalable row is fully considered when designing AQS, so all synchronizers built on AQS in JUC can obtain this advantage.

  • The main use of AQS is inheritance, and subclasses manage synchronization state by inheriting the synchronizer and implementing its abstract methods.

  • AQS uses an int type member variable state to represent the synchronization state. When state>0, it means that the lock has been acquired, and when state = 0, it means that the lock is released. It provides three methods (getState(), setState(int newState), compareAndSetState(int expect, int update)) to operate the synchronization state state, of course, AQS can ensure that the operation of the state is safe.

  • AQS completes the queuing of resource acquisition threads through the built-in FIFO synchronization queue. If the current thread fails to acquire the synchronization state, AQS will construct the current thread and waiting state and other information into a node (Node) and add it to the synchronization queue. Also blocks the current thread

  • When the synchronization state is released, the thread in the node is woken up to try to obtain the synchronization state again.

Implementation of CLH Synchronized Queue

In the CLH synchronization queue, a node represents a thread, which saves the thread's reference (thread), status (waitStatus), predecessor node (prev), and successor node (next), which are defined as follows:

static final class Node {
    /** 共享 */
    static final Node SHARED = new Node();

    /** 独占 */
    static final Node EXCLUSIVE = null;

    /**
     * 因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态;
     */
    static final int CANCELLED =  1;

    /**
     * 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
     */
    static final int SIGNAL    = -1;

    /**
     * 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,改节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
     */
    static final int CONDITION = -2;

    /**
     * 表示下一次共享式同步状态获取将会无条件地传播下去
     */
    static final int PROPAGATE = -3;

    /** 等待状态 */
    volatile int waitStatus;

    /** 前驱节点 */
    volatile Node prev;

    /** 后继节点 */
    volatile Node next;

    /** 获取同步状态的线程 */
    volatile Thread thread;

    Node nextWaiter;

    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {
    }

    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

Status (waitStatus)

  • CANCELLED : The value is 1. Since the thread waiting in the synchronization queue is timed out or interrupted, it needs to cancel the waiting from the synchronization queue, and the node will not change when it enters this state.
  • SIGNAL: The value is -1, the thread of the successor node is in the waiting state, and if the thread of the current node releases the synchronization state or is canceled, it will notify the successor node, so that the thread of the successor node can run.
  • CONDITION: The value is -1, the node is in the waiting queue, and the node thread is waiting on the Condition. When other threads call the signal() method on the Condition, the node will be transferred from the waiting queue to the synchronization queue and added to the pair. Acquiring synchronization status.
  • Propagate: The value is -3, indicating that the next shared synchronization state acquisition will be propagated unconditionally.
  • Initial: The value is 0, the initial state.

join the team

Enter image description

  • compareAndSetTail(Node expect, Node update) method to ensure that nodes can be added thread-safely.
  • enq (final Node node), the synchronizer ensures the correct addition of nodes through an "infinite loop". In the "infinite loop", the current thread can return from this method only after the node is set as a tail node through CAS, otherwise, the current thread The thread keeps trying to set.
  • acquireQueued, enter a spin process, each node is observing introspectively, when the conditions are met and the synchronization state is acquired, it can exit from this spin process.

First, the head node is the node that successfully obtained the synchronization state. After the thread of the head node releases the synchronization state, it will wake up its successor node. After the thread of the successor node is woken up, it needs to check whether its predecessor node is the head node. Node.

Second, maintain the FIFO principle of synchronous queues.

AQS: Block and wake up threads

If the thread fails to acquire the synchronization state, it joins the CLH synchronization queue, and continuously acquires the synchronization state by spinning. However, during the process of spinning, it is necessary to determine whether the current thread needs to be blocked. The main method is acquireQueued() :

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;

Through this code, we can see that after the failure to obtain the synchronization status, the thread is not blocked immediately, and the status of the thread needs to be checked. The method to check the status is the shouldParkAfterFailedAcquire(Node pred, Node node) method, which is mainly in the front. The driver node determines whether the current thread should be blocked. The code is as follows:

  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前驱节点
        int ws = pred.waitStatus;
        //状态为signal,表示当前线程处于等待状态,直接放回true
        if (ws == Node.SIGNAL)
            return true;
        //前驱节点状态 > 0 ,则为Cancelled,表明该节点已经超时或者被中断了,需要从同步队列中取消
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } 
        //前驱节点状态为Condition、propagate
        else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

This code mainly checks whether the current thread needs to be blocked. The specific rules are as follows:

  • If the status of the precursor node of the current thread is SINNAL, it indicates that the current thread needs to be blocked, call the unpark() method to wake up, and return true directly, and the current thread is blocked

  • If the predecessor node status of the current thread is CANCELLED (ws > 0), it indicates that the predecessor node of the thread has waited for a timeout or has been interrupted, and the predecessor node needs to be deleted from the CLH queue until backtracking to the predecessor node state< = 0 , returns false

  • If the predecessor node is not SINNAL or CANCELLED, set its predecessor node to SINNAL by CAS and return false

If the shouldParkAfterFailedAcquire(Node pred, Node node) method returns true, call the parkAndCheckInterrupt() method to block the current thread:

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

The parkAndCheckInterrupt() method mainly suspends the current thread, thereby blocking the call stack of the thread, and returns the interrupt status of the current thread. Internally, the park() method of the LockSupport tool class is called to block the method.

When the thread releases the synchronization state, it needs to wake up the thread's successor node:

 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
				//唤醒后继节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

Call unparkSuccessor(Node node) to wake up the successor node:

 private void unparkSuccessor(Node node) {
        //当前节点状态
        int ws = node.waitStatus;
        //当前状态 < 0 则设置为 0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //当前节点的后继节点
        Node s = node.next;
        //后继节点为null或者其状态 > 0 (超时或者被中断了)
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从tail节点来找可用节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //唤醒后继节点
        if (s != null)
            LockSupport.unpark(s.thread);
    }

There may be cases where the successor node of the current thread is null, timed out, or interrupted. If this happens, you need to skip the node, but why start from the tail node instead of node.next? ? The reason is that node.next may still be null or canceled, so the tail backtracking method is used to find the first available thread. Finally, call the unpark(Thread thread) method of LockSupport to wake up the thread.

Link

AQS: Block and wake up threads

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325053974&siteId=291194637