深入理解AQS实现原理

文章目录

      • AQS使用模板设计模式
      • AQS中的数据结构-节点和同步队列
      • 节点在队列中的移动
      • ReentrantLock的公平锁的可重入实现原理
      • ReentrantReadWriteLock的可重入实现原理:
      • 锁的获取与释放过程详细讲解
        • 获取锁的时序图:
        • A线程获取锁:
        • B线程此时再来获取锁:
        • A线程释放锁的过程:
        • 将B线程唤醒:

AQS,全名AbstractQueuedSynchronizer,从名字中可以看出,这是一个抽象类,内部维护了一个同步队列,事实正是如此。

在AQS中最重要的是一个state变量。private volatile int state;这个state的意思由继承这个类的子类来决定,用来表示锁的状态,如在ReentrantLock中可以使用0表示没有线程持有该锁,1表示该锁已经被其他线程持有。当然在可重入锁的实现时还会进行累加,代表当前线程持有锁的数量,这些在接下来都会一一讲到。

AQS使用模板设计模式

  • 独占式的获取锁模板方法:
    • accquire()
    • acquireInterruptibly():允许被打断
    • tryAcquireNanos():尝试获取并加入了超时
  • 共享式获取锁:
    • acquireShared
    • acquireSharedInterruptibly
    • tryAcquireSharedNanos
  • 独占式释放锁:
    • release()
  • 共享式释放锁:
    • releaseShared

需要子类覆盖的流程方法:

  • 独占式获取:tryAcquire
  • 独占式释放:tryRelease
  • 共享式获取:tryAcquireShared
  • 共享式释放:tryReleaseShared
  • 这个同步器是否属于独占模式:isHeldExclusively

同步状态state

  • getState:获取当前的同步状态
  • setState:设置同步状态
  • compareAndSetState:使用CAS设置状态,保证状态设置的原子性

AQS中的数据结构-节点和同步队列

在解读AQS之前,先来了解一下它的数据结构,它有一个内部类Node,该内部类定义如下:

static final class Node {
        static final AbstractQueuedSynchronizer.Node SHARED = new AbstractQueuedSynchronizer.Node();
        static final AbstractQueuedSynchronizer.Node EXCLUSIVE = null;
        static final int CANCELLED = 1;//线程等待超时或者被中断了,需要从队列中移出
        static final int SIGNAL = -1;//后续的节点等待状态,当前节点通知后面的节点运行
        static final int CONDITION = -2;//当前节点处于等待队列
        static final int PROPAGATE = -3;//用在共享里,表示状态要向后传播
        volatile int waitStatus;
        volatile AbstractQueuedSynchronizer.Node prev;
        volatile AbstractQueuedSynchronizer.Node next;
        volatile Thread thread;
        AbstractQueuedSynchronizer.Node nextWaiter;
        }

在AQS中是有一个同步队列的,这个同步队列的每个节点Node的定义如上,在这个node中保留了一个Thread线程,所以这个队列是一个线程队列,也就是说AQS会把申请锁的线程封装成一个node类型的节点,加入到同步队列中。而且还有两个prev和next分别指向前驱节点和后继节点,也就说明了这是一个双向队列。

深入理解AQS实现原理_第1张图片

  • 节点是用来保存争夺锁失败的线程,然后把它们放到一个同步队列中,当锁被释放时,再从同步队列中依次取出线程获得锁。
  • 流程:线程获取锁,如果获取成功,就执行并且退出返回了。如果获取不成功,就把它封装成一个节点,放到同步队列的队尾,这个时候是需要CAS设置的。然后进入同步队列之后,就开始自旋,判断前驱节点是不是头结点,如果是就获取同步状态,获取失败或者前驱不是头结点,线程就继续等待。直到获取成功,然后当前节点设置为头节点。
  • 当一个节点放到尾节点的时候,需要用CAS,因为有可能是多个节点同时往尾节点上加,而设置头节点时却不需要,因为每次只有一个。

在AQS中还有一个内部类,ConditionObject实现了Condition接口

public class ConditionObject implements Condition, Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        private transient AbstractQueuedSynchronizer.Node firstWaiter;
        private transient AbstractQueuedSynchronizer.Node lastWaiter;
        private static final int REINTERRUPT = 1;
        private static final int THROW_IE = -1;

为什么要使用notifyAll和signal:
condition对应的是等待队列,一个condition对应一个等待队列。而wait和notify中同样维护了一个同步队列,但是它只有一个等待队列。在调用wait进入等待队列的时候,不能确保哪个线程处于该等待队列的头结点,所有唤醒的时候如果只是使用notify唤醒一个头结点,有可能会出现信号丢失,也就是唤醒了错误的线程(该线程唤醒后发现条件不满足,又继续进行等待。)。所以要使用notifyAll把所有线程都唤醒。然后这些线程根据条件判断是否运行还是继续等待。
而在condition中只需要使用signal就可以,这是因为挂在某个condition队列中的节点都是满足被唤醒的条件的,所以只需要唤醒一个头节点即可,唤醒多了也没有用。

节点在队列中的移动

await方法:

深入理解AQS实现原理_第2张图片

当一个线程获取锁后,调用了await方法,将调用await方法的节点从同步队列中移除,并把它放到等待队列的尾节点。

signal方法:

深入理解AQS实现原理_第3张图片

当一个节点被通知时,它会从等待队列中被移出,然后尝试去获取锁,如果获取不到,就会被加入同步队列中等待后续的获取。

ReentrantLock的公平锁的可重入实现原理

深入理解AQS实现原理_第4张图片

锁的可重入释放锁的实现原理

深入理解AQS实现原理_第5张图片

ReentrantReadWriteLock的可重入实现原理:

我们知道,ReentrantReadWriteLock是有两个锁,读锁和写锁,那么如何通过一个标志位state来同时表示读锁和写锁呢,int类型的在计算机中是有32位的,就使用高位的16位表示写锁状态,低16位表示写锁的状态。对于写锁来说,只能被一个线程获取到,写锁的重入也和上面ReentrantLock的原理一致。但是读锁可以同时被很多个线程获取到,在高16位中可以表示被哪些线程获取了,那么每个线程重入了几次该如何表示呢,那么就需要每个读锁的线程维护一个自己的ThreadLock,在这里面保存每个线程的重入状态。

锁的获取与释放过程详细讲解

获取锁的时序图:

深入理解AQS实现原理_第6张图片

A线程获取锁:

第一次A线程获取锁的时候,非公平锁调用AQS的acquire方法

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

然后acquire内部调用ReentrantLock中的内部类NonfairSync中的tryAcquire方法,其实是调用了nonfairTryAcquire方法,

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;
        }

如果当前锁的标准是0,代表可以获取锁,就通过CAS将标志位state改为1,并且将当前线程设置为独占锁的线程,返回true结束。这时A线程已经获取到了锁,并开始运行。

深入理解AQS实现原理_第7张图片

B线程此时再来获取锁:

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

这时尝试获取锁会失败tryAcquire为false,就会执行addWaiter(Node.EXCLUSIVE),将该线程加入等待队列

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;
    }

首先将当前线程封装成一个Node,一开始头结点head和尾节点tail都为空,所以直接进入enq()方法

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;
                }
            }
        }
    }

这个方法中是一个自旋,尾节点如果为空,就CAS创建一个头节点节点,并且head和tail都指向这个节点,如上图步骤2
深入理解AQS实现原理_第8张图片

第二次自旋,将当前线程的节点的前驱指针指向上一步创建的头结点,然后将tail指向当前线程节点,并把头节点的尾指针指向当前线程节点,这样就把当前线程节点加入到了同步队列中,如上图步骤3.
在这里插入图片描述

addWaiter方法执行完之后会返回一个Node,然后传入到acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中,传入的Node再试图抢一下锁或者阻塞。

/**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//拿到它的前驱节点
                if (p == head && tryAcquire(arg)) {//如果前驱节点是头节点,就再试一下拿锁。
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//如果没有拿到锁,判断它是否需要挂起
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

对获取锁失败的节点,检查并且更新状态,判断是否需要阻塞

/**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)//如果前驱节点的等待状态为SIGNAL,表示当使用锁的线程释放锁的时候会通知它来运行,那么node就可以进行阻塞了,因为它的前驱节点还在等待中。
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {//如果前驱节点状态为cancelled,就把前驱节点从同步队列中移除掉
            /*
             * 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;
    }

如过前驱节点不是SIGNAL状态和CANCELED状态,就执行compareAndSetWaitStatus方法去改变前驱节点的状态

/**
     * CAS waitStatus field of a node.
     */
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

unsafe.compareAndSwapInt这个方法会将前驱节点的状态改为SIGNAL。图中红色的为阻塞线程
深入理解AQS实现原理_第9张图片
这是shouldParkAfterFailedAcquire的作用,将前驱节点的状态改为SIGNAL,并且返回true。然后判断parkAndCheckInterrupt,该方法将当前线程挂起。并且返回是否被中断过,如果有被中断过,这个线程被唤醒后,就会响应中断 interrupted = true;

 /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

A线程释放锁的过程:

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

release方法首先会调用tryRelease方法,主要有两个作用,将当前拥有锁的线程设为null,并将state赋值为0.

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//重入的释放
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);//获取锁的线程为null
            }
            setState(c);//state=0
            return free;
        }

将B线程唤醒:

接下来要唤醒处于等待队列中挂起的线程,release方法继续执行,判断 if (h != null && h.waitStatus != 0)head是否为空,并且waitStatus!=0.此时head不为空,并且waitStatus为SIGNAL就是-1.所有要执行unparkSuccessor(head)

 /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {//传进来的是head头节点
      
        int ws = node.waitStatus;
        if (ws < 0)//头节点的waitStatus为SIGNAL就是-1.改为0就是initialized
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {//这里是当后继节点为空或者被取消了,就有将后继节点移除出去。
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)//这里从尾部开始遍历,是为了防止断链。因为在将一个节点放入
                //到队列的时候,需要三步,1.将当前节点的pre指向前驱节点,2.将tail指向当前节点,3.将前驱节点的Next指向当前节点。由于这三步不是一个原子操作,有可能将	前驱节点取消的时候第三步还没有完成,这个时候就断链了。
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)//s就是B节点
            LockSupport.unpark(s.thread);//将B节点的线程唤醒。
    }

将B线程唤醒之后,由于当初B线程是在方法acquireQueued中被阻塞的,所以唤醒之后继续在该方法内自旋:

  final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//拿到它的前驱节点
                if (p == head && tryAcquire(arg)) {//如果前驱节点是头节点,就再试一下拿锁。
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//阻塞在这里了,现在唤醒了
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

自旋再次 if (p == head && tryAcquire(arg)),此时tryAcquire就可以拿到锁了。拿到锁之后,将当前节点设为头节点,并把之前的节点删除了。如图:

深入理解AQS实现原理_第10张图片

你可能感兴趣的:(并发编程)