全网最硬核的源码分析之—AQS源码分析(上 独占模式)

147 阅读7分钟

介绍

Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)包括ThreadPoolExecutor的Worker都是基于AQS实现的。

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

数据结构

node的详细数据在下方重要变量

1.1 重要变量-和链表的node

// 头结点, 当前持有锁的线程 
private transient volatile Node head;

// 阻塞的尾节点,每个新的节点进来,都插入到最后,
private transient volatile Node tail;

// 代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
private volatile int state;

// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer

static final class Node {
        /** 表示线程以共享的模式等待锁*/
        static final Node SHARED = new Node();
        /** 表示线程正在以独占的方式等待锁*/
        static final Node EXCLUSIVE = null;

        /** 表示线程获取锁的请求已经取消 */
        static final int CANCELLED =  1;
        /** 表示线程已经准备好了,等待资源释放 */
        static final int SIGNAL    = -1;
        /** 表示节点在等待队列中,节点线程等待唤醒 */
        static final int CONDITION = -2;
        /**
         * 当前线程处在SHARED情况下,该字段才会使用
         */
        static final int PROPAGATE = -3;

        /**
         * 当前节点在队列中的状态 值为上方的数字
         *   
         */
        volatile int waitStatus;

        /**
         * 前驱指针
         */
        volatile Node prev;

        /**
         * 后继指针
         */
        volatile Node next;

        /**
         *表示处于该节点的线程
         */
        volatile Thread thread;

        /**
         * 指向下一个处于CONDITION状态的节点
         */
        Node nextWaiter;

       
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * 返回前驱节点,没有的话抛出npe
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

    
    }

独占模式重点方法

获取锁流程流程简述

AQS 独享模式获取锁流程为 调用acquire传入次数 然后AQS会先调用实现类的tryAcquire方法尝试获取锁

如果获取失败会通过addWaiter和end方法将当前线程包装为一个node并加入双向链表中 加入完成会通过acquireQueued方法将线程挂起,或者获取锁

acquire- 尝试设置获取锁 并设置值 

   public final void acquire(int arg) {
   // 获取资源成功或者新增一个独占类型节点到同步等待队列成功则直接返回,否则中断当前线程
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //中断当前线程
            selfInterrupt();
    }

addWaiter --将当前线程包装为node添加到链表 

private Node addWaiter(Node mode) {
//将当前线程和模式创建一个节点
Node node = new Node(Thread.currentThread(), mode);
//tali是队列尾节点 
Node pred = tail;
//如果尾节点不为空 
if (pred != null) {
 // 将当前的队尾节点,设置为自己的前驱 
    node.prev = pred;
      // 用CAS把自己设置为队尾, 如果成功后,tail == node 了,这个节点成为阻塞队列新的尾巴
    if (compareAndSetTail(pred, node)) {‘
        // 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连
        pred.next = node;
        return node;
    }
}

 // pred==null(队列是空的) 或者 CAS失败(有线程在竞争入队)
enq(node);
return node;
}

enq-队列是空的 或者 有线程在竞争入队死循环入队

     // 采用自旋的方式入队
    // CAS设置tail过程中,竞争一次竞争不到,就多次竞争,总会排到的
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 队列为空
            if (t == null) {
                // 初始化head节点
                if (compareAndSetHead(new Node()))
                    // 把tail指向head,设置完了以后,继续for循环,下次就到下面的else分支了
                    tail = head;
            } else {
                // 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

tryAcquire --交由实现类实现 可以灵活实现自己的获取锁逻辑

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

acquireQueued-线程挂起,然后被唤醒后去获取锁

  // 下面这个方法,参数node,经过addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列
    // 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,
    // 意味着上面这段代码将进入selfInterrupt()打断当前线程,所以正常情况下,下面应该返回false
    // 线程挂起,被唤醒后去获取锁
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取前驱节点
                final Node p = node.predecessor();
                // p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
                // 注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列
                // 所以当前节点可以去试抢一下锁
                // 这里我们说一下,为什么可以去试试:
                // 首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,
                // enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程
                // 也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 到这里,说明上面的if分支没有成功,要么当前node本来就不是队头,
                // 要么就是tryAcquire(arg)没有抢赢别人,继续往下看
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 什么时候 failed 会为 true???
            // tryAcquire() 方法抛异常的情况
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire-处理前驱节点

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 当前处理节点的前驱节点的等待状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 前驱节点状态设置成Node.SIGNAL成功,等待被release调用释放,后继节点可以安全地进入阻塞状态
        return true;
    if (ws > 0) {
        // ws大于0只有一种情况Node.CANCELLED,说明前驱节点已经取消获取资源,
        // 这个时候会把所有这类型取消的前驱节点移除,找到一个非取消的节点重新通过next引用连接当前节点
        do {
           node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 其他等待状态直接修改前驱节点等待状态为Node.SIGNAL
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt-阻塞当前线程,获取并且重置线程的中断标记位

// 阻塞当前线程,获取并且重置线程的中断标记位
private final boolean parkAndCheckInterrupt() {
    // 这个就是阻塞线程的实现,依赖Unsafe的API
    LockSupport.park(this);
    return Thread.interrupted();
}

selfInterrupt-打断当前线程

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

解锁锁流程流程简述

AQS 独享模式解锁流程为 调用release传入次数 然后AQS会先调用实现类的tryRelease方法尝试解锁 如果成功拿到链表的头节点,头节点不为空并且状态不是初始化状态,则解除传入节点的第一个后继节点的阻塞状态

release

  public final boolean release(int arg) {
 // 尝试释放资源
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease

// 尝试释放资源,独占模式下,尝试通过重新设置status的值从而实现释放资源的功能
// 这个方法必须由子类实现
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

unparkSuccessor --解除传入节点(一般是头节点)的第一个后继节点的阻塞状态

// 解除传入节点(一般是头节点)的第一个后继节点的阻塞状态,当前处理节点的等待状态会被CAS更新为0
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 当前处理的节点(一般是头节点)状态小于0则直接CAS更新为0
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 如果节点的第一个后继节点为null或者等待状态大于0(取消),则从等待队列的尾节点向前遍历,
        // 找到最后一个不为null,并且等待状态小于等于0的节点
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    // 解除上面的搜索到的节点的阻塞状态
    if (s != null)
        LockSupport.unpark(s.thread);
}