ReenterLock源码解析


本文只解析加锁解锁部分,忽略Condition部分。以公平锁为例。

锁的基本思想

锁的目的

要让同一时间只能有一个线程占有共享的资源。

锁的实现思路

如何达到这个目的呢?
需要考虑1.用什么作为锁,2.如何获得锁(同时考虑:如何阻塞其他线程),3.如何释放锁(同时考虑:如何唤醒其他线程)?

ReentreLock实现

顺着这个思路,带着三个问题,来看ReenterLock的实现:

1.state作为锁;
2.当state=1时表示获得锁,=0表示锁空闲

此时其他线程如何阻塞?
并发场景按照线程之间的执行程度分为两种情形:
用电影票售票场景举例,售票窗口是共享资源,那么:
2.1.交替执行。(CAS尝试获取锁)
假设一个非热卖点的售票时间,人不算多。
有两个人A,B。A先到,正在办理售票(占用资源),后一个人也在赶来,当B赶到,A已经受理结束了。虽然并发,但是线程之间请求资源的时间点有先后顺序,阻塞时间忽略不计。
2.2.争抢执行。(队列)
假设一个假期恰好有个火热的电影上映,人特别多。
有五六个人同时到达办理窗口,他们谁先谁后,怎么知道呢,售票窗口小姐姐喊道“排队!”,这时候阻塞明显,好吧只能通过排队去办理了。
所以:

尝试获取锁的方式有两种:CAS,排队。
3.双向链表的队列存储需要唤醒的线程

如何唤醒排队中的人呢?需要队头办好业务的节点去唤醒下一个节点上的线程,而线程中的节点有可能会取消掉了,需要调整队伍。而列表中要知道节点的上一个节点,单向列表需要记住上一节点的位置下标,重头遍历才能找到,而双向列表只需访问上一指针。所以使用了双向链表,提高查询效率。

ReenterLock的内存分配

1.初始化

在这里插入图片描述

2.队列维护空间

head和tail的作用是:
head:用来指向队头第一个办理业务的人;
tail:用来记录排在队尾的人,再有人来排队需要排在队尾。
在这里插入图片描述
需要排队:
由于排队中的成员都是被阻塞的,睡眠的,不运行的,不能自己察觉到办理窗口是否可用,需要等待正在办理业务的人来唤醒队列中的人。正在受理的人必须是队列中的成员,他才知道要叫醒谁,也就是说他是一个节点他才知道要叫醒谁。
因此:队列在初始化时会创建一个正在持有锁的线程作为对头。

3.队列初始化

假设锁被线程t1占用,即state=1,队列初始化内存空间如下图所示:
在这里插入图片描述

4.入队

线程进入阻塞队列,就是双向链表添加节点的操作一样:
假设现在持有锁的线程是t1,排在后面的是t2
正在有个线程t3加入,加入步骤是:
t3将作为队尾,但是先要把t3链接到当前队列:tail.next=t3;
t3.prev=tail;
t3作为队尾:tail=t3.
此时空间结构如下图所示:
在这里插入图片描述

来看源码

lock()

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

如下我将三个方法的流程图画下来
tryAcquire,addWaiter,acquireQueued

tryAcquire/尝试获得锁
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }

在这里插入图片描述

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
        Node pred = tail;
        //队列是否为空
        if (pred != null) {
        //不为空则直接插入队尾
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //队列为空则设置队头(添加一个thread=null作为持有线程的节点),再将当前线程的节点放在队尾
        enq(node);
        return node;
    }

在这里插入图片描述

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);
        }
    }
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //waitStatus标志已经前一节点已经检查过一次了,前一节点状态是等待状态,不用再往前问也不尝试拿锁,可直接park了。
        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 {
            //跳过状态为cancel的节点,往前一个节点连接
                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;
    }

此方法目的是再试拿一下锁,拿不到则自己阻塞。
sinal状态的作用:标志已经前一节点已经检查过一次了,前一节点状态是等待状态,不用再往前问也不尝试拿锁,可直接park了。
橙色线头为死循环,只有获取到锁,或者调用park阻塞,才跳出循环
在这里插入图片描述

unlock()

释放锁需要做的事情:
1.设置state=0;(但是有重入的情况,这里跟加锁对应,加锁是state+1,那么这里是state-1)
2.设置持有锁的线程变量=null;
3.唤醒下一个节点。(这里有个要注意的地方,当前节点的下一个节点如果为空,是从尾部向前查询,拿到最邻近自己的睡眠节点,那么问题来了,为什么是尾部查询。原因我已经注释在代码上了)

        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;
                //2.设置持有锁的线程变量=null;
                setExclusiveOwnerThread(null);
            }
            //1.设置state=0;
            setState(c);
            return free;
        }
 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;
            //*** 【原因】:执行到这里,如果其他线程获取到当前节点,并在当前节点后加了节点node2,那么继续向后查询就查不到node2了,那么久没办法唤醒node2,要等到下一个线程持有再释放锁时再来唤醒node2,这无非降低了效率。
            //从节点尾部向前查,查最靠近当前节点的等待状态的节点。
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        //3.唤醒下一个节点。
            LockSupport.unpark(s.thread);
    }

总的一句话,ReenterLock是基于volatile+CAS+park+自旋 实现的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值