Java AQS源码学习

整体认识

自我理解

为了哪些基于FIFO阻塞队列并且依赖于一个int类型状态属性的同步器提供基础的构建框架。这里的重点是

  • FIFO阻塞队列
  • int类型的状态位

基于该类定制的同步器,要明确的定义状态的意义,也就是0代表什么,1代表什么,还有其它。这里就提供了拓展点,让用户自定义。
并且使用getState()、setState()、compareAndSetState()方法来操作int类型的状态标志属性,并且要重新定义一下方法(这里分类独占模式和共享模式):

  • tryAcquire()
  • tryRelease()
  • tryAcquireShared()
  • tryTeleaseShared()
  • isHeldExclusively()

该类支持两种模式,独占模式和共享模式,对于该类的子类,没必要支持这两种模式,只要支持其中一种就可以,也就是上面的方法,如果子类要实现独占模式,就没必要重新定义共享模式的方法,只需要重新定义tryAcquire()和tryRelease()和isHeldExclusively()方法。

大神还在注释中列举了两种模式的使用例子,其实基于AQS可以定制自己的同步器。知识Java提供了经典的同步器,Sychronized和ReentrantLock,这两个是独占模式的。对于共享模式的,有CountDownLatch等。在日常情况下是够用的,如果不能满足业务要求,就自己基于AQS定制同步器。

关键点

对于一些字段的状态进行修改,使用的是CAS原子操作,CAS在计算机指令来说就是一条指令。
首先要搞懂一点,队列是一个双向队列的FIFO的队列,关于队列,就是尾结点插入,头结点取出。头指针总是指向队列中的第一个节点(要判断是否有头结点),尾结点指向双向节点的最后一个节点。

先来了解一下state各个状态的含义;

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

0:新结点入队时的默认状态。

1. 唤醒子节点线程

当前节点释放,一定要释放器后继节点

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        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);
    }

从队列的尾节点向前查找,查找到当前节点的下一个节点为止。这里有一个问题,为什么要从后往前查询而不是从前往后查询呢?
查找到该节点,就将该节点唤醒。

2. 排他的获取锁

这个方法很关键,当前节点自旋的获取锁,能有资格自旋获取锁的线程一定是当前头结点的下一个节点,其它节点都没有资格。
当前线程获取锁,就将该线程设置为头结点,然后退出
但是也有没有资格的线程自旋的获取锁,处理逻辑在shouldParkAfterFailedAcquire(p, node)中。
逻辑为:如果当前节点的前驱节点的状态为SIGNAL,表示该节点为没有资格获取锁,他的前驱节点都不是头结点,他肯定没有资格。

如果没有找到合适的前驱,当前线程就不能被挂起。在等待过程中,该线程被中断,就将该线程中断标志位改成true,也就是标识,已经被中断了。一开始我还没动shouldParkAfterFailedAcquire方法中的逻辑,其实就是在判断该线程是否正在等待,是否等待是通过前驱节点来判断的,如果前驱节点的状态是SIGNAL,那该节点一定处于等待中,就直接返回true了,表示处于等待中。parkAndCheckInterrupt()方法就是判断该线程是否被中断过,如果是,再加上前面的处于等待,就将该线程的中断标志位表示为true。(在parkAndCheckInterrupt中调用了isInterrupted(),判断了中断位是否为true,但是也把中断位重置了,所以才要在外层显示的将中断位设置为true)

如果前驱节点的状态>0,为CANCELLED状态 ,说明前驱节点已经被取消调度了,也就是不能在该队列中混了,这样就不能作为当前节点的前驱节点,也就是要跳过这些废的节点,重新找前驱节点。当找到合适前驱节点,更改前驱节点状态为SIGNAL。

无论如何,都要执行取消自旋获取锁,这个方法不多讲。

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)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

3. 其它

该类中还有其它方法,但是都是很容易理解。
因为AQS知识独占模式和共享模式,所以获取锁和释放锁都是有这两个版本的。

我个人觉得尝试获取锁是AQS的精华,要理解透彻。在学习之前,一定要牢牢把握一点,AQS是一个状态位,一个FIFO的双向队列。标志位来判断当前线程的状态,更具不同的状态有不同的操作,双向队列就是保持所有要竞争锁的线程,通过Node节点来封装。

还有解释一点,只有头结点的下一个节点有获取锁的资格(这也是队列的特点,先进先出原则),其它节点是没有的。还要定期清理队列,因为有的线程,慢慢的就退出了竞争锁的行列,不玩了,那肯定要把这些节点清理掉,这个操作就是在自旋获取锁这个方法中执行的。

4. 关于CAS底层实现

AQS的主要实现是用CAS,CAS是很多锁基础,是cynchronized基础,但是查看CAS源码发现,很快就调用了JVM方法,这里就看看JVM方法。

其实最终的本质就是调用了一条汇编指令cmpxchg,lock cmpxchg 后者在多处理器使用

下面就是CAS的核心JVM源码了,不在多说

// 支持CAS的操作,
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
  assert(sizeof(jbyte) == 1, "assumption.");
  // 目标属性地址,也就是在对象中的偏移地址
  uintptr_t dest_addr = (uintptr_t)dest;
  uintptr_t offset = dest_addr % sizeof(jint);
  // 获取对象当前值地址,这是一个指针
  volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
  // 对象当前值
  jint cur = *dest_int;
  // 当前值得地址
  jbyte* cur_as_bytes = (jbyte*)(&cur);
  jint new_val = cur;
  // new_val地址
  jbyte* new_val_as_bytes = (jbyte*)(&new_val);
  // new_val存exchange_value,后面修改则直接从new_val中取值
  new_val_as_bytes[offset] = exchange_value;
  // cas关键流程,如果期待值和当前值相同  
  while (cur_as_bytes[offset] == compare_value) {
    // 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_val
    jint res = cmpxchg(new_val, dest_int, cur);
    if (res == cur) break;
    cur = res;
    new_val = cur;
    new_val_as_bytes[offset] = exchange_value;
  }
  // 返回当前值
  return cur_as_bytes[offset];
}

疑惑点

  • Node类中waitState和state有什么区别

参考资料

参考资料-01

参考资料-02

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值