多线程系列学习:AQS(一)获取锁

AQS是AbstractQueuedSynchronizer的简称,juc包下锁的实现,基本上需要借助于AQS的功能,如下图所示:

通过继承结构,可以看到,常用的可重入锁ReentrantLock,以及同步辅助工具类CountDownLatch、Semaphore,都用到了AQS。

首先要明白:锁分为独占锁和共享锁,独占锁又分为公平锁和非公平锁。

  1. 独占锁:所谓独占锁,就是某一时刻,该锁只能被一个线程占有的
  2. 共享锁:共享锁则是某一时刻,该锁可以被多个线程同时占有
  3. 公平锁:获取锁的顺序,和请求锁的顺序是一致的
  4. 非公平锁:获取锁的顺序,和请求锁的顺序不一致,存在抢占机制。

下面将通过ReentrantLock中的代码,以公平锁为例,聊聊AQS获取锁的过程。先看ReentrantLock类的结构,如下图所示:

可以发现,ReentrantLock中,有3个内部类:Sync、FairSync(公平锁的实现)和NonfairSync(非公平锁的实现),并且FairSync和NonfairSync继承于Sync,而Sync继承了AQS:

以FairSync实现为例,我们加锁一般是通过lock()方法,该方法也是获取锁的顶级入口,看其实现:

        final void lock() {
            acquire(1);
        }

可以看到,它调用了acquire()方法:

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */    
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

根据注释可知:

  1. acquire()方法,获取的是独占锁,并且忽略中断
  2. tryAcquire()方法,尝试获取锁,至少会执行一次
  3. 如果tryAcquire()方法尝试获取锁失败,则通过addWaiter()方法,将当前线程放入同步队列
  4. acquireQueued()方法,可能会重复经历阻塞、唤醒、尝试获取锁的过程,直到成功获取锁

接下来,主要是分析这3个方法:

  1. tryAcquire(int arg)
  2. addWaiter(Node node)
  3. acquireQueued(addWaiter(final Node node, int arg)

1. 尝试获取锁:tryAcquire(int arg)

        protected final boolean tryAcquire(int acquires) {
            // 当前线程
            final Thread current = Thread.currentThread();
            // 同步锁状态
            int c = getState();
            // 如果状态为0,则表示没有线程持有该锁
            if (c == 0) {
                // 队列的前面没有等待锁的线程,通过CAS设置锁的状态为acquires,也就是1
                // 并且标记当前线程是占有该锁的线程
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果占有锁的线程是当前线程
            else if (current == getExclusiveOwnerThread()) {
                // next是锁将要更新的状态,可以看到,值是累加的,这也就说明了该锁是可重入锁
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
               // 设置锁的状态
                setState(nextc);
                return true;
            }
            return false;
        }

 注释在代码中了,其流程大概如下:

  1. 如果同步锁的状态state为0,则并且队列种没有比当前线程等待更久的线程,则尝试获取锁
  2. 如果当前线程是占有该锁的线程,不需要重新获取锁,只需要更新锁的状态(累加1),实现了可重入

 

2. 将线程添加到同步队列:addWaiter(Node node)

    /**
     * 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);
        Node pred = tail;
        // 如果尾节点不为空(也就是链表不为空),则将当前节点插入到链表的末尾,成为新的尾节点
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 通过enq()方法,添加到链表的末尾
        enq(node);
        return node;
    }

通过注释以及代码,可以知道:

  1. addWaiter()方法的作用就是将当前线程包装成节点,并添加到链表的末尾
  2. 添加到链表的末尾的两条路径:(1)如果链表不为空,则通过尾节点链接 (2)如果链表为空则通过enq()方法添加

看下enq()方法:

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

可以看到,enq()方法通过死循环,也就是自旋的方式来设置的

 

3. 获取锁: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) {
        // 前一个节点的等待状态
        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.
             */
            // 向前找,直到找到一个waitStatus不是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.
             */
            // waitStatus的值为0(初始状态)或者是PROPAGATE
            // 设置waitStatus的值为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

可以看出,只有当前一个节点的状态为SIGNAL时,才会阻塞当前节点所包装的线程。

总结一下获取锁的过程:

  1. 通过tryAcquire(int arg),尝试获取锁,只尝试了一次,所以有可能没有获取成功
  2. 如果tryAcquire(int arg)没有获取成功,则通过 addWaiter(Node node)将线程封装成Node节点,链接到链表的末尾
  3. 通过acquireQueued(addWaiter(final Node node, int arg)的死循环,不断尝试获取锁,直到成功

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值