java进阶-7-D -多线程-Lock专题- AQS 之 获取锁:acquire / acquireQueued

20 篇文章 0 订阅

AQS 获取锁源码解析

没图片,我从不BB

                                                                        

1.锁最频繁的2大操作之一:获取 acquire  - (以独占式锁为例)

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

结合上面的故事说明: 

  1. 用户调用lock,执行获取锁方法(顾客到按摩店前台,招呼老板娘安排技师
  2. 执行内部方法 acquire --> 内部调模版方法 tryAcquire (老板娘查看技师是否作业中,是否有顾客在服务):
    1. tryAcquire返回true,获取到锁:end (情况1:技师空闲,无顾客在服务-->登记顾客,技师工作中
    2. 在锁被占用的时                                ( 情况2:技师工作中,有顾客服务中--->提醒顾客取号):
      1. 先执行addWaiter,将当前Node到队尾(顾客先领取老板娘的号码牌,进入排队中
      2. 然后执行AcquireQueued方法,该方法目的有二:( 排队顾客不时问老板娘:下一个是轮到我吗?技师有空了没?
        1. 拦截所有不是Head-Next的节点,保证在队列中的线程获取锁顺序的公平性(老板娘回答:下一个轮到1号,给我看下你的号码牌: --->1.是1号哟,里面请。  2.不是,继续等着吧) 
        2. 所有等待队列的线程进入到Park状态,状态为calceled=1的线程会从等待队列中剔除(老板娘每次也会你前一个人是否走,走的号码牌就注销
 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;
    }
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);
        }
    }

shouldParkAfterFailedAcquire   和 parkAndCheckInterrupt  这2个东西要放到一起来看

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //从这里我们就知道: NodeA.status=SIGNAL表示 下一个 NodeA.next 是要被 PARK 的
            return true;    //true : 对就是你赶紧下车!!!
        if (ws > 0) {
            //就从不断循环,不断将 node.prev指向更前面没有Canceled的node,我们知道,这个地方是为 
           //了删除掉那些已经canceled的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //能走到这 ,pred =0/ <-1 ,要是等于0 说明这是一个新加入的节点,那在给一次tryAcquire 
            //的机会, 要是<-1 那说明这个节点是 await进来 or 这是个共享传播锁,那必须在给一 
            //次  tryAcquire  机会
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;    //false : 表示本次不Park 
    }

No Picture no BB

不是说这个解释错,大伙时间这么宝贵,不想浪费大家时间

通过一个真实的的案例来带我们理解这个acquire的工作: 

  1.  实例化一个ReentrantLock的锁之后,开启10个线程,每个线程加一把锁并且执行的工作是休眠2分钟,这里还是要强调一下,同一把锁每次只能有一个线程占有,开10个线程,每60s开启一个,第一次启动,线程直接通过tryAcquire()获取到锁,执行休眠  ,再然后其他的线程会每隔60s进来,此时线程会尝试去拿锁,失败之后会进入到等待队列中,等待被唤醒!
  2. 上一步最后面说的尝试去拿锁是 通过tryAcquire方法,这个方法是一个在继承AQS抽象类之后自己主动去实现的类。在尝试拿锁失败之后就会去执行 acquireQueued(addWaiter(null),arg) 
  3. 执行 acquireQueued(addWaiter(null),arg) 先要去执行addWaiter(null)  表示在线程获取锁失败之后要将Thread 包装成Node,然后存放到 等待队列的尾巴上,(不用担心等待队列是否为空,这个队列是个双向队列,保证了获取锁的顺序也就是公平性) 此时nodeA就会进入到acquireQueued() 方法中
  4. 此时acquireQueued 拿到了已经放到尾巴上的nodeA 然后进行自旋{  先去把这个nodeA 的前驱节点nodePre拿到 ,看一下前驱节点是头节点吗,是 -- 那就再次尝试获取锁。【拿到了那就释放掉这个nodeA,over /没拿到那就继续往下看】 ,不是那就继续执行下面的判断 shouldParkAfterFailedAcquire(nodePre,nodeA), && ParkAndCheckInterrupt() .
  5. 进入到shouldParkAfterFailedAcquire(nodePre,nodeA) 方法,此时nodePre是head ,waitStatus  int 类型 默认值是0。0的情况会进入到if的第三个判断,里面的操作是将nodePre的waitStatus 设置成 -1,返回false,这样在acquireQueued 的自旋进行第二次的时候,判断nodePre的waitStatus = -1,返回true,会调用 ParkAndCheckInterrupt() , 可以防止一直运行自旋,消耗资源。此时就算是已经将任务提交到等待队列中了。
  6. 要是这个时候又有新的线程进入, 就是去重复 2~5 的动作。     一个Lock使用完之后会调用unlock方法,这就是将等待队列中的Park状态给打断 ,此时队列中Head -Next ->nodeB 就可能tryAcquire 成功,然后nodeB-Next->nodeC ,nodeC 就成为Head-Next->nodeC ,就可以在下一次锁竞争中 tryAcquire 了。   基本上就是这个样子了

主要的东西都已经说明了,在理解的过程中碰到的几个问题

  • 新Node的waitStatus未设置值吗?是的(老板娘一开始肯定笑脸相迎,允许你问2次 "是否叫d到号了")
  • AcquireQueue 中有for(;;) , 不断运行,会不会占用很高资源?不用担心,里面有叫停功能,LockSupport.park(老板娘会惯着你一直问有没有姑娘空吗???)

2.讲了acquire 那可以顺提一下release 方法

在理解了acquire的实现方式之后,在理解release 就非常简单了:

在调用unLock之后当前占用的线程表示占用结束,下一个线程可以去竞争锁了,在公平锁的情况下,锁的释放过程 就是一个将等待队列的Head-Next的nodeA进行 unPark,waitStatus=0(unparkSuccessor),并且将Head-Next 指向 nodeA-Next  = nodeB  ,这个时候 要是nodeA 执行tryAcquire成功,就会执行nodeA的线程,然后修改nodeA  ---> prev=null,thread=null (acquireQueued) ,简单吧,共享式的有细微的差别,唤醒动作是共享传播式

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值