ReentrantLock源码分析

ReentrantLock是JUC中最常用的一种重入锁,其内部实现原理是通过一种叫AQS的队列来控制并发。
我们先来讨论一下锁的基本原理,多线程环境下,如果要共同访问一个资源,有这么两种情况:

  1. 线程先后交替执行,不存在竞争,一个线程拿到同步代码的执行权(即获取锁)直接执行即可,无需将自己挂起。
  2. 多个线程同时执行,存在竞争,只有一个线程可以拿到锁,而其他线程就乖乖排队,并且把自己挂起,等那个线程执行完了再从队列中唤醒一个排队的线程。

根据上面的分析,我们可以得出实现锁需要一个排队的队列,以及线程的挂起、唤醒机制。然后我们再来看看ReentrantLock是怎样实现这些机制的。
我们先看加锁过程,它的lock方法:

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

它调用了sync的lock方法,这个sync是什么:
sync
由上面的层次结构图可知,Sync继承了AbstractQueuedSynchronizer(简称AQS),Sync之下还有两个子类FairSync和NonfairSync(公平锁和非公平锁)。AQS顾名思义是抽象队列同步器,也就是我们上文提到的排队队列,公平锁和非公平锁我稍后再做解释。
sync的lock方法是抽象的,具体实现交由它的两个子类,我们先以FairSync的实现为例:

        final void lock() {
            acquire(1);  //记住这里传入了1
        }

AQS中的acquire方法:

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

acquire方法是获取锁的总体方法,我们来看它做了哪些步骤,首先调用tryAcquire方法,不过这个tryAcquire在AQS中只是抛了一个异常,并没有具体的实现,所以还得看具体子类的实现,还拿FairSync为例:

        protected final boolean tryAcquire(int acquires) {
        	//获取当前线程
            final Thread current = Thread.currentThread();
            //获取一个state值,这个state可以看作当前锁是否被持有的状态,
            //如果是0则说明没有线程持有锁
            int c = getState();
            //无锁状态
            if (c == 0) {
                if (!hasQueuedPredecessors() &&  //判断自己是否需要排队
                    compareAndSetState(0, acquires)) {  //如果不需要排队则通过CAS获取锁
                    setExclusiveOwnerThread(current);  //获取成功则将独占线程设置为当前线程
                    return true;
                }
            }
            //如果已有线程获取锁,则判断当前独占线程是不是自己
            else if (current == getExclusiveOwnerThread()) {
                //如果是自己,则将当前state值+1(acquires传入了1)
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                //重新设置state值,
                //这里体现了“可重入”的概念,当同一个线程再次获取锁时,不需要挂起,只需要将state+1即可
                setState(nextc);
                return true;
            }
            return false;
        }

在tryAcquire中,有一个重要方法hasQueuedPredecessors,这个方法的作用是判断当前线程是否需要排队,来看看它的实现:

    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

这里首先会获取一个头节点和尾节点,这两个节点是什么?我们需要先看看AQS到底是个什么结构:
AQS
它有两个Node类型的成员变量head和tail,这个Node类型又包括如下内容:
Node
其中prev表示当前node的前置节点,next是后置节点,thread表示当前node绑定的线程对象,可见AQS是一种双向链表的结构。
然后回到hasQueuedPredecessors方法,在线程先后交替执行的时候,并没有线程需要排队,所以当前队列是空的,head和tail都为null,这时h != t为false,不会再执行后面的判断,该方法返回。
然后又调用compareAndSetState方法,通过CAS设置state值,CAS如果不了解的话自行百度吧,网上关于它的讲解太多了。如果CAS成功,再将当前AQS中的独占线程设置为当前线程,即获取锁成功。

刚才讨论的是线程交替执行,没有初始化队列的情况,如果是多个线程同时执行,存在竞争,这里又该怎样判断呢?
如果有多个线程竞争,就会形成队列,此时h!=t成立,这时就会进入后面的判断,首先判断(s = h.next) == null,也就是头结点的下一个是否为null,很明显不是,然后又判断s.thread != Thread.currentThread(),也就是头结点的下一个节点是否为当前线程,如果是则返回true,否则返回false。

说明:

  1. 为什么要判断头结点的下一个节点,而不是判断头结点?
    因为在AQS中,头结点永远是持有锁的节点,该节点绑定的线程处于运行状态。只有头节点之后的线程才是真正被挂起的排队线程。
  2. 如果队列刚被初始化,第一个参与排队的线程是头结点吗?
    不是,队列初始化时,会先new一个thread为null的虚拟节点为头结点,因为头结点永远是持有锁的线程,不能让排队的线程当作头节点。
  3. 为什么最后要判断s是否等于当前线程?
    如果s等于当前线程,则说明当前尝试获取锁的线程是第一个排队的线程,方法会返回false,然后会尝试CAS获取锁,如果此时头节点刚好释放锁,则直接获取成功,不需要再进入队列。
    就好比你去火车站买票,如果窗口后面第二个人是你的好朋友,你可以直接让他帮你买,刚好这时前面那个人买完票了,就轮到好朋友给你买票了。

分析完tryAcquire方法,再回到acquire,看剩下的逻辑:

        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();

如果tryAcquire失败,则调用acquireQueued,同时里面又调用了addWaiter方法,这个addWaiter方法就是用来把线程放到队列里的。

    private Node addWaiter(Node mode) {
    	//new一个node,并且绑定线程为当前线程
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //如果是第一次调用,pred==null,会直接调用后面的enq方法
        if (pred != null) {
            node.prev = pred;
            //如果不是第一次调用,利用CAS将尾节点替换为node,如果替换不成功,说明有竞争,再进入enq方法
            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
            	//如果是第一次调用,tail为null,此时会new一个新的node设为head
            	//这里就是上文提到的:队列初始化时的虚拟头节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                //否则利用CAS将尾节点替换为node
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

当添加到队列中后,别忘了还有一件事,就是把线程挂起,这就是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)) {
                	//如果获取成功则将头节点设为node
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //判断自己是否应该被挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这里看一下shouldParkAfterFailedAcquire方法:

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

在Node中,有这样几个状态:

        //表示该节点被取消
        static final int CANCELLED =  1;
        //表示该节点需要被唤醒
        static final int SIGNAL    = -1;
        //表示该节点需要等待条件唤醒
        static final int CONDITION = -2;
        //向后传播
        static final int PROPAGATE = -3;

节点在初始化的时候,waitStatus是等于0的,所以在第一次执行shouldParkAfterFailedAcquire方法是,会进入else分支,这时会用CAS将前一个节点状态设置为SIGNAL,然后返回false;
接着进入下一轮循环,同样还是获取它前一个节点,再次进入shouldParkAfterFailedAcquire,这次ws==Node.SIGNAL,返回true,

为什么这里要循环两次?
第一次是为了将前一个节点状态设为SIGNAL,第二次确认一下确实置为SIGNAL了就返回。
为什么要设置前一个节点状态,而不是自己?
前一个节点的线程当前处于挂起状态,需要后一个节点辅助。

然后执行parkAndCheckInterrupt:

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

注意,这里调用的park方法,就是挂起线程用的,LockSupport其实是个工具类,真正提供挂起功能的,是Unsafe类的park方法。(Unsafe类如果不了解,自行百度吧,总之是JDK里很底层,很牛逼的一个类)

至此,加锁过程分析完了,然后我们再来看解锁过程。
unlock方法:

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

sync的release方法:

    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:

        protected final boolean tryRelease(int releases) {
        	//当前state-1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
            	//如果c等于0,表示当前没有线程持有锁
            	//因为是可重入的,所以同一个线程可能需要解锁多次才能减为0
                free = true;
                //将当前独占线程清空
                setExclusiveOwnerThread(null);
            }
            //重新设置state值
            setState(c);
            return free;
        }

tryRelease成功后,获取当前头节点,也就是正在解锁的线程,此时正常情况下h!=null成立,h.waitStatus应该为SIGNAL,不等于0,执行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;
        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;
        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);
    }

s被赋值为头节点的下一个节点,如果还有线程在排队,则s==null不成立,判断s.waitStatus > 0,只有状态为CANCELLED时才大于0,也就是被取消,这时需要从尾节点开始,从后往前循环,找到第一个状态小于0的节点,赋值给s。
然后,如果s不为null,说明还有可唤醒的节点,再调用unpark方法解锁,同样该方法是Unsafe类提供的。
释放锁的过程分析完毕。

然后现在还有一个问题,被挂起的线程,当被唤醒后,从哪儿开始执行?
当然是在哪儿挂起的,在哪儿恢复。它是在acquireQueued方法中,调用parkAndCheckInterrupt方法时挂起的,为了方便,再贴一次代码:

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

parkAndCheckInterrupt执行完成后,进入下一轮循环,又进行了tryAcquire操作,此时它可以成功获取锁,因为头节点已经释放锁了,然后将头节点设为自己,如此往复。

还没完,刚才我们一直分析的都是公平锁,那么非公平锁又是怎样一个操作?
直接看NonfairSync的lock方法:

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

可以看出非公平锁上来就进行一次CAS操作获取锁,如果成功则直接将当前独占线程设为自己。否则再调用acquire方法。
我们看看非公平锁的tryAcquire实现:

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

里面调用的是nonfairTryAcquire:

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	//与公平锁不同的是,这里直接CAS设置状态,不需要调用hasQueuedPredecessors
                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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值