java并发编程的艺术(6)深入挖掘aqs独占锁源码

并发编程里面的很多常用的类,例如ReentrantLock,Semaphore,CountDownLatch实际上底层都是通过使用AbstractQueuedSynchronizer(AQS)来进行实现的。那么今天我们就来仔细聊聊AQS这样东西。

底层的核心主要是维护一个volatile int waitStatus的状态值,以及一个FIFO线程等待队列。对于waitStatus变量,AQS里面提供了三种方式:

  • getState()
  • setState()
  • compareAndSetState()

同时在AQS里面定义了两种对于资源访问的方式,独占模式和共享模式。简单来说,独占模式就是一次只能由一个线程执行,例如常见了ReetrantLock,共享模式则是允许多个线程同时执行,例如说Semaphore,CountDownLatch。

AQS同步器里面经常会用到以下的几种方法:

//独占模式中常用的一种获取资源的函数
boolean tryAcquire(int arg)

//独占模式中常用的一种释放资源的函数
boolean tryRelease(int arg)

//该线程是否正在独占资源。只有用到condition才需要去实现它
boolean isHeldExclusively()

//共享模式中常用到的获取资源的方式
int tryAcquireShared(int arg)

//共享模式中常用到的释放资源的方式
boolean tryReleaseShared(int arg)

源码分析模块:
我们首先来模拟一个场景进行思维导向。
在独占模式下一个线程请求获取资源的过程:
1.首先是进入aqs里面的public final void acquire(int arg) 这个函数中
来看看里面的源码先:

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

首先是通过tryAcquire()请求获取资源,如果成功则直接结束。

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

通过一个for循环来不断地tryAcquire()来获取资源
再继续看源码tryAcquire()部分,这里面的代码内容是抛出一个异常,因为AQS本身是一个框架,对于具体的获取资源处理,它将其交给了开发者去自定义处理。

同步队列

同步器内部的数据结构其实是一个基于FIFO原则来设计的双向队列:
在这里插入图片描述

对于其中的每个node节点,都有一个叫做waitStatus的变量来表示当前线程的状态。查看源码可以看到这四种状态:
在这里插入图片描述

  1. CANCELLED:表示被中断或者取消的状态
  2. SIGNAL:表示前面的节点已经释放了同步锁,等待被唤醒的一个状态
  3. CONDITION:表示处于等待状态
  4. PROPAGATE:表示处于共享模式中的运行状态

AQS里面对于插入队尾的操作和我们常用的list集合插入操作有所出入,为了保证队列操作的原子性和有序性,源码里面采用了cas自旋的方式来实现。

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

这一段代码和核心还是使用了unsafe类里面的接口来指定内存进行分配。

通过对于上述代码的分析,我们来一段小结先:
当多个线程同时发出请求的时候,会发生资源竞争(tryAcquire()和addWaiter()),所有请求的线程会按照先来后到的规矩排成一条队列,刚刚上述的分析主要是针对于获取锁的过程而言。那么对于未获取到资源的线程而言,下一步又应该处理什么呢?
接着我们来看到上边提到的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; // 将引用对象置空,有利于GC             
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

final Node p = node.predecessor();是获取当前等待节点的前一个节点,也就是即将释放锁的节点元素。这里面的核心主要还是通过for循环的方式进行自旋操作,不断地请求资源,一方获取资源成功之后,便将该节点的next和pre还有thread先置空,然后进入shouldParkAfterFailedAcquire(p, node)当中,继续深入源码分析:
这里可以结合我们前边提及到的waitStatus状态数值来进行分析

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * 节点处于待唤醒状态
         */
        return true;
    if (ws > 0) {
        /*
         * 前驱节点被中断了,于是继续查找前驱节点,查看是否有元素满足要求
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 这里面也是调用了unsafe类里面的cas操作来进行节点状态的更新
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

直到进行到parkAndCheckInterrupt()函数里面,才算是真正的让该线程进入休眠状态。

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

说完了获取锁的整个思路,再来看看锁的释放吧。
在同步队列里面的node节点元素汇总,有个waitStatus变量:
volatile int waitStatus;
这里之所以用volatile来进行修饰的原因,我个人认为是volatile修饰的关键字在进行修改了之后,会向其他线程发送信号,重新从主存中读取该值,保证了该变量在各个线程之间的可见性。

private void unparkSuccessor(Node node) {
   
    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);
}

这段代码中的核心部分为LockSupport.unpark(s.thread);主要是起到解除阻塞线程的一个作用,通俗点来说,就是唤醒队列中正在等待的下一个节点。

结合上述的独占式模式的一个分析思路,对于共享模式的分析也是大同小异了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值