ReentrantLock与AQS的源码学习笔记

前言

由ReentrantLock揭开AQS的大幕
先来段代码让我们更好的进入到ReentrantLock的世界

class A {
    
    
    ReentrantLock lock = new ReentrantLock();
    public void run(){
    
    
        try{
    
    
            lock.lock();
            //执行业务
        }finally {
    
    
            lock.unlock();
        }
    }
}

上面这段代码,当运行到lock.lock()的时候,ReentrantLock是如何保证线程安全的呢?接下来就一起去揭秘ReentrantLock底层是怎么保证线程安全的吧!

概述

要谈ReentrantLock,那就不得不谈下AbstractQueuedSynchronizer(AQS)!我们从ReentrantLock的Sync开始谈起,慢慢深入AQS!

先解释下AQS,它定义了一套多线程访问共享资源的同步器框架。很多同步类的实现都依赖于它,就比如接下来我们说的ReentrantLock。

ReentrantLock底层实现了一套自定义的同步队列器-Sync,我们来看看它的源码

//它是ReentrantLock实现同步控制的根本,它的子类有fair和non-fair两个,一个是公平锁,一个是非公平,各有各的实现方式。这个基类借助AQS的state来展示持有锁的数量
abstract static class Sync extends AbstractQueuedSynchronizer {
    
    
    private static final long serialVersionUID = -5179523762034025860L;

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    abstract void lock();

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    //非公平锁尝试获取资源的方法
    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;
    }

    //尝试释放资源
    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);
        }
        setState(c);
        return free;
    }

    //判断当前线程是否是持有资源的排他线程
    protected final boolean isHeldExclusively() {
    
    
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    //TODO:回头补一下
    final ConditionObject newCondition() {
    
    
        return new ConditionObject();
    }

    // Methods relayed from outer class
	//获取拥有资源的线程
    final Thread getOwner() {
    
    
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
	//持有锁的数量
    final int getHoldCount() {
    
    
        return isHeldExclusively() ? getState() : 0;
    }
	//判断是否当前资源被占有
    final boolean isLocked() {
    
    
        return getState() != 0;
    }

    /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
    //防止破坏单例
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    
    
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

里面定义了多个方法,我们重点需要掌握它的lock和tryAcquire方法,接下来我们看看它的两个子类

FairSync

//公平锁
static final class FairSync extends Sync {
    
    
    private static final long serialVersionUID = -3000897897090466540L;

    //加锁,这里的acquire是调用的AQS的方法,它有自己的一套流程
    final void lock() {
    
    
        acquire(1);
    }

    //公平锁尝试获取资源的方法
    protected final boolean tryAcquire(int acquires) {
    
    
        final Thread current = Thread.currentThread();
        int c = getState();
        //看看当前state是否是0,如果是0说明现在没有线程去获取
        if (c == 0) {
    
    
            //它很老实,需要先看下同步队列中是否也有想要获取相同资源的节点,如果有的话,就需要AQS的一套流程,入队列等
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
    
    
                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;
    }
}

NonFairSync

//非公平锁
static final class NonfairSync extends Sync {
    
    
    private static final long serialVersionUID = 7316153563782823691L;

    //非公平锁,上来先去尝试一下获取资源,如果不成功,再走AQS那一套
    final void lock() {
    
    
        //如果抢占成功,就可以直接设置占有了。
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //否则乖乖听话,走我AQS的那一套
            acquire(1);
    }
    //尝试获取的方式,它是直接调用Sync的nonfairTryAcquire方法
    protected final boolean tryAcquire(int acquires) {
    
    
        return nonfairTryAcquire(acquires);
    }
}

其实ReentrantLock加锁机制就是:我有两种实现方式,一是公平锁机制,而是非公平锁机制,对于队列同步器各有一套方案,ReentrantLock只需要定义获取state的方式和释放方式即可,至于那些具体线程的队列的维护(获取失败入队和唤醒队列)都已经再AQS顶层封装好了。接下来我们来看下AQS框架吧!

AQS框架

AQS内部维护了一个Volatile int state的变量和一个FIFO的等待队列(线程获取资源失败后进入阻塞所加入的队列),访问state的方式有三种:

  • getState():获取当前state的大小,就是当前持有多少个锁
  • setState():设置state的大小
  • compareAndSetState():通过CAS操作设置state的大小,它是线程安全的。

接下来就需要了解下AQS里几个比较重要的方法了:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

AQS定义两种资源共享方式:一种是独占资源,一种是共享资源,在当我们去自定义同步队列器的时候,我们并不是将上面的方法全部重写,而是相对应的进行重写:当我们定义的是独占资源的话,仅需要去实现tryAcquire(int)和tryRelease(int)方法即可;当我们定义的时候共享资源的话,仅需要器实现tryAcquireShared(int)和tryReleaseShared(int)方法即可。

这篇文章主要讲的是ReentrantLock,我们就拿ReentrantLock的资源方式来讲解,因为它是独占资源的方式,接下来我们就只需要分析独占资源重写的两种方式即可。

源码分析

在这一段,我们从acquire–>release的次序来!

在这里先把队列的节点的几种状态先来列举一下,方便后来的分析:

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

不知道大家还记不记得在前面我们看源码的东西:acquire(1),这个方法,它是AQS已经写好的方法,接下来我们去看看它内部是怎样的。

//获取排他锁,忽略掉中断的线程
public final void acquire(int arg) {
    
    
    //这里的几个函数是关键
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

函数流程是这样的:

  1. tryAcquire:这个就是我们之前看到的ReentrantLock下Sync的子类实现的,公平锁和非公平锁各有一套。获取失败返回false
  2. addWaiter(Node.EXCLUSIVE):这是获取资源失败以后的操作,让当前阻塞线程入队列,设置独占模式的节点
  3. acquireQueued():这个函数的作用就是,让当前节点不断的自旋尝试获取资源,直到成功!
  4. selfInterrupt():能进入这个函数说明,当前这个线程获取到资源了,不过这个线程在获取到资源之前被中断过,正常来讲,它是不被响应的,所以出来之后,再进行自我中断,把中断补上。

之前我们已经看过tryAcquire()了,现在我们直接从addWaiter开始看起

addWaiter:

/**
 * Creates and enqueues node for current thread and given mode.
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
//创建一个节点,将节点入队列
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入队
    enq(node);
    return node;
}

再来看enq的源码:

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
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操作,确保节点能够安全的加入到队尾当中。

再回来看下acquireQueued:从刚开始的获取资源失败,然后将节点加入到队尾,接下来线程该做什么了?那肯定是进入到休息状态,等待被唤醒然后去获取资源,干自己想干的事情了,这个函数非常关键,我们通过源码一行一行的分析:

/**
 * 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变量用于表示当前节点是否获取资源失败的状态
    boolean failed = true;
    try {
    
    
        //标记当前节点对应的线程是否被中断过,默认没有被中断
        boolean interrupted = false;
        //接下来就是进入到一个死循环了,直到获取资源才返回
        for (;;) {
    
    
            //找到当前节点的前去节点
            final Node p = node.predecessor();
            //这个前驱节点是head节点吗?如果是的话,当前线程就可以尝试着再去获取一遍锁了
            //是头节点并且获取资源成功            
            if (p == head && tryAcquire(arg)) {
    
    
                //将获取资源成功,那就将当前这个节点设置为head节点
                setHead(node);
                //将它原先的前驱结点与它断开联系,方便回收
                p.next = null; // help GC
                //更改标志位,表示当前节点获取资源成功了
                failed = false;
                //返回当前节点在被唤醒前是否被中断过
                return interrupted;
            }
            //第一个函数用来判断当前节点是否可以安心去休息了,如果可以安心秀习,调用第二个函数进入线程进入阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //醒来之后发现当前线程被中断过,那就更改下标志,方便后续进行的自我中断
                interrupted = true;
        }
    } finally {
    
    
        // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
        if (failed)
            cancelAcquire(node);
    }
}

讲完这个函数,我们来看看他里面的另外几个函数:

shouldParkAfterFailedAcquire: 它是用来判断当前节点是否可以安心的去休息了,假如我前面的节点已经放弃了,只是瞎站着,那么我还是有机会去获取锁的。我们来看看源码:

/**
 * 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;
    //如果是SIGNAL的话,我就可以安心的去休息了
    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.
             */
        //设置前驱结点的状态为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    //返回false是不确定前驱节点的状态是否是SIGNAL,需要再通过一遍循环来判断,最终确认一下才可以去休息
    return false;
}

再来看下parkAndCheckInterrupt: 它是在前面函数确保当前节点可以去休息了,那么这个就是真正的去操作节点休息的函数。

/**
 * Convenience method to park and then check if interrupted
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
    
    
    LockSupport.park(this);//阻塞当前线程
    return Thread.interrupted();//返回该线程被唤醒之前是否被中断过
}

小结一下:

  1. 节点在加入到队尾后,检查状态,找到休息的安全点
  2. 调用park进入到wait状态,等待unpark()或interrupt()唤醒自己
  3. 被唤醒后,看看自己是否有资格获取资源,如果自己之前被中断过,就没有资格去获取资源,没有的话,就继续第一步流程

最后总结一下整个acquire的流程:

  1. 先来调用一下自定义同步器的tryAcquire(),尝试获取一遍资源,如果失败进入流程2;
  2. 将当前线程封装成一个节点加入到AQS维护的FIFO队列的尾部,并标记为独占模式;
  3. acquireQueued()是线程在等待队列中休息,如果有机会(轮到自己会被unpark())会去获取资源。获取到资源后才返回,如果在此期间当前线程被中断过就返回true,否则返回false;

最后再来张流程图加深一遍印象:

至此,整个acquire(1)的流程就结束了!

上面是独占锁的抢占过程以及抢占不成功入队列的过程,下面就来看看独占锁的释放资源的过程。

先来看看顶层入口:release方法

/**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
public final boolean release(int arg) {
    
    
    //尝试着去释放资源,如果成功就往下走,否则直接返回
    if (tryRelease(arg)) {
    
    
        Node h = head;//找到头结点
        if (h != null && h.waitStatus != 0)//这是下一个节点需要去唤醒的条件:头结点不为空或者当前节点状态为0
            //去唤醒下一个节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

大家可能会想,去唤醒下一个节点为什么是那样的状态呢?我就说下我在分析的时候是怎么理解的吧:首先,当前节点的状态默认是0,这是大家都知道的。然后,这个节点的状态是由下一个节点是否阻塞来设置的,在获取资源的时候我们了解到,当要获取资源的状态获取失败的时候,它会去找一个安全点然后去阻塞,这个安全点就是让它的前一个节点状态设为SIGNAL。回到我们现在想要知道的缘由,所以,我想要去唤醒我的下一个节点的前提就是,我当前节点的状态不能为0和不能为空,为空说明我没有下一个节点了,自然就不用去唤醒了,为0说明我下一个节点现在没有阻塞,自然也不用去唤醒了。

说完上面的,接下来我们再去看下tryRelease方法

//这个方法是AQS交给自定义同步器去实现的
protected boolean tryRelease(int arg) {
    
    
    throw new UnsupportedOperationException();
}
//来看看这个ReentrantLock实现的。
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;
}

再接着Release往下看unparkSuccessor这个方法:

如果资源被完全释放的话,当前节点就可以去唤醒下一个节点的状态了,我们来看看它的源码

/**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
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;
    if (ws < 0)//如果当前节点的状态是小于0,先去设置状态为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;//得到这个节点的后继节点
    if (s == null || s.waitStatus > 0) {
    
    //如果该节点为null或者状态>0就不唤醒
        s = null;
        //从后往前找,找到最后一个可以被唤醒的节点,唤醒它。
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //唤醒该节点,这里就跟前面的parkAndCheckInterrupt方法对上了。
        LockSupport.unpark(s.thread);
}

至此,我们的ReentrantLock的加锁解锁的机制就完全结束了。接下来回去分析下AQS下的共享锁机制,后期补上笔记

鸣谢

文章灵感来自这篇博客:https://www.cnblogs.com/waterystone/p/4920797.html,文章写的很好。

猜你喜欢

转载自blog.csdn.net/MarkusZhang/article/details/107995364