ReentrantLock和AbstractQueuedSynchronizer源码笔记

刚开始只准备写ReentrantLock,但发现ReentrantLock和AbstractQueuedSynchronizer关联太多,所以两个类一起写了。这篇文章ReentrantLock为主,AbstractQueuedSynchronizer为辅。AbstractQueuedSynchronizer中大多数方法都会放到ReentrantLock中去介绍。

先来AbstractQueuedSynchronizer:

AbstractQueuedSynchronizer:

CLH队列

AbstractQueuedSynchronizer维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列,线程在获取锁失败时会进入该队列,并且不停地自旋尝试获取锁(acquireQueued()),只有当前节点的前驱节点是头节点才会被唤醒,也就是队列的第二个节点,如果失败则会阻塞自己,直至被唤醒,而当持有锁的线程释放锁时,会唤醒队列中的后继线程

state变量

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程调用lock()时,会调用tryAcquire()独占该锁并将state+1。此后,B线程和C线程再次尝试获取锁时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。而可重入的概念值A线程释放锁之前,它自己是可以重复获取此锁的(state会累加)。但是获取锁多少次就要释放多少次,这样才能保证state能回到0。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

队列节点

当线程获取资源失败时会进入等待队列,此时线程会被封装成一个Node节点,

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;

        //条件等待
        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() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

变量waitStatus

其中变量waitStatus则表示当前Node结点的等待状态,共有5种取值:

  • CANCELLED (1):当前线程因为超时或者中断被取消。这是一个终结态,也就是状态到此为止。
  • SIGNAL (-1):当前线程的后继线程被阻塞或者即将被阻塞,当前线程释放锁或者取消后需要唤醒后继线程。这个状态一般都是后继线程来设置前驱节点的。
  • CONDITION (-2):当前线程在condition队列中。
  • PROPAGATE (-3):用于将唤醒后继线程传递下去,这个状态的引入是为了完善和增强共享锁的唤醒机制。在一个节点成为头节点之前,是不会跃迁为此状态的
  • 0:表示无状态。
    在源码中很多地方都是通过>0、<0来判断结点的状态是否正常

头尾节点

    //头结点
    private transient volatile Node head;

    //尾节点
    private transient volatile Node tail;

AQS中包含了head和tail两个Node引用,其中head在逻辑上的含义是当前持有锁的线程,head节点实际上是一个虚节点,本身并不会存储线程信息。也称为哨兵节点,尝试获取锁失败而被加入到同步队列时,会用CAS来设置尾节点tail为当前线程对应的Node节点。
head和tail在AQS中是延迟初始化的,也就是在需要的时候才会被初始化,也就意味着在所有线程都能获取到锁的情况下,队列中的head和tail都会是null。

AbstractQueuedSynchronizer更多方法在ReentrantLock当中进行阐述

ReentrantLock:

ReentrantLock有一个静态内部类Sync。

静态内部类Sync

abstract static class Sync extends AbstractQueuedSynchronizer {
        //留给子类实现 非公平和公平锁
        abstract void lock();
        //非公平的尝试获取独占锁
        final boolean nonfairTryAcquire(int acquires) {
            ////获取当前线程
            final Thread current = Thread.currentThread();
            //获取重入次数
            int c = getState();
            //0表示独占锁还没被获取
            if (c == 0) {
                //cas尝试设置重入次数 也就是尝试获取独占锁
                if (compareAndSetState(0, acquires)) {
                    //获取独占锁成功 设置当前线程为独占线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //独占锁已被获取 判断当前线程是不是独占线程
            else if (current == getExclusiveOwnerThread()) {
                //是的话  重入次数加acquires
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //设置重入次数
                setState(nextc);
                return true;
            }
            return false;
        }
        //尝试释放独占锁
        //如果释放完全 state为0 返回true 否者返回false
        protected final boolean tryRelease(int releases) {
            ////得到重入次数
            int c = getState() - releases;
            //当前线程不是独占线程 直接抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //重入次数等于0 说明独占锁可以释放
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //设置重入次数
            setState(c);
            return free;
        }
}

Sync是公平锁和非公平锁实现的基础,非公平锁和公平锁都是其子类,并使用AQS的state变量来代表锁被持有的次数
附上一段ReentrantLock使用示例:

public class ReentrantLockTest {

    public static void main(String[] args) throws Exception{
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try{
            //TODO
        }catch (Exception e){
            lock.unlock();
        }finally {
            lock.unlock();
        }
    }
}

我们先看像获取锁操作,也就是lock.lock();

获取锁

非公平锁

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        /**
         *  尝试获取独占锁
         */
        final void lock() {
            //cas设置state的值 0->1
            if (compareAndSetState(0, 1))
                //cas设置成功 然后设置独占线程为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                //调用AQS中的模板方法 尝试获取独占锁
                //最终会调用tryAcquire尝试获取独占锁
            else
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

上面这段代码你需要结合Sync父类AbstractQueuedSynchronizer中的acquire()方法一起来看

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

我们可以得知非公平锁获取锁步骤:

1:调用lock方法,lock方法首先会进行CAS操作,将state属性尝试设置为1,成功则代表获取到锁, 然后将exclusiveOwnerThread属性设置当前线程为独占线程。
2:倘若失败获取锁失败,调用Sync父类AbstractQueuedSynchronizer#acquire()方法,其中tryAcquire(arg)方法则是调用Sync子类NonfairSync和FairSync的实现(各自分别实现是为了区分出公平锁和非公平锁获取锁的逻辑不同,在后面会有介绍)
3:在这里非公平锁的tryAcquire()实现则是调用Sync的本身方法nonfairTryAcquire()

这里很绕,但是相信你看源码便一目了然,在这里我们先把acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个重要方法放一放,因为公平锁的获取锁的步骤跟非公平锁类似,调用函数也差不多,我先写公平锁方便做一个比较:

公平锁

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            //会调用tryAcquire尝试获取独占锁
            acquire(1);
        }
        /**
         *  尝试获取锁 阻塞队列如果没有阻塞线程 尝试获取
         *  成功返回true 失败返回false
         */
        protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取重入次数
            int c = getState();
            //重入次数为0表示独占锁还没被获取
            if (c == 0) {
                //这里是公平的 前面已经有阻塞线程了我就不尝试获取独占锁了
                //否者阻塞队列中没有阻塞节点就会尝试cas设置重入次数
                //非堵塞和堵塞就这里有区别  !hasQueuedPredecessors()
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    //cas设置重入次数成功就把当前线程设置为独占线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //锁已经被线程占用 判断当前的线程是不是独占线程 是的话 就增加可重入次数
            else if (current == getExclusiveOwnerThread()) {
                //增加重入次数
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                //设置重入的次数
                setState(nextc);
                return true;
            }
            return false;
        }
    }

获取锁的步骤跟非公平锁一样,只是FairSync自己实现了tryAcquire()方法,并未调用Sync类的方法,但其实这两个方法代码真的很类似,唯一不同一点在于FairSync在拿独占锁添加了这个判断

//判断AbstractQueuedSynchronizer的队列是否有等待线程
!hasQueuedPredecessors() 

这也是体现了公平和非公平的实质,公平锁发现等待队列有值时,我会添加到线程尾部(这是后面addWaiter()方法实现),非公平则不会判断这一点。
接下来我们看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法:

addWaiter()

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //快速的enq(添加node到队尾),否则走完整的enq(node)
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //cas把新节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return 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;
                }
            }
        }
    }

addWaiter(Node.EXCLUSIVE)是AbstractQueuedSynchronizer中的方法,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列。倘若队列没有初始化则会在enq()方法中通过自旋和cas操作新建队列,并将当前节点加入队尾并返回。倘若已初始化,直接加入队尾并返回。

acquireQueued()

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;//标记是否成功获取锁
        try {
            boolean interrupted = false;//标记线程是否被中断过
            //这是一个自旋,会不断尝试让自己挂起
            for (;;) {
                final Node p = node.predecessor();//获取前驱节点
                //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);// 获取成功,将当前节点设置为head节点
                    p.next = null; // help GC 原head节点出队,在某个时间点被GC回收
                    failed = false;//获取成功
                    return interrupted;//返回是否被中断过
                }
                // 判断获取失败后是否可以挂起,若可以则挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 线程若被中断,设置interrupted为true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    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.
             */
            // 前驱节点状态为signal,返回true
            return true;
        // 前驱节点状态为CANCELLED
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            // 从队尾向前寻找第一个状态不为CANCELLED的节点
            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.
             */
            // 将前驱节点的状态设置为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,此时前驱节点出队列后会将该节点唤醒。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。

释放锁

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

先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

关于AbstractQueuedSynchronizer中的unparkSuccessor():

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        // 尝试将node的等待状态置为0,这样的话,后继争用线程可以有机会再尝试获取一次锁。
        if (ws < 0)
            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;
        /*
         * 这里的逻辑就是如果node.next存在并且状态不为取消,则直接唤醒s即可
         * 否则需要从tail开始向前找到node之后最近的非取消节点。
         *
         * 这里为什么要从tail开始向前查找也是值得琢磨的:
         * 如果读到s == null,不代表node就为tail,参考addWaiter以及enq函数中的我的注释。
         * 不妨考虑到如下场景:
         * 1. node某时刻为tail
         * 2. 有新线程通过addWaiter中的if分支或者enq方法添加自己
         * 3. compareAndSetTail成功
         * 4. 此时这里的Node s = node.next读出来s == null,但事实上node已经不是tail,它有后继了!
         */
        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);
    }

你可能感兴趣的:(ReentrantLock和AbstractQueuedSynchronizer源码笔记)