全程高能!AQS源码解析

让我们来聊聊AQS

AQS框架是在Java并发中很重要的一个框架,CountDownLatch、ReentrantLock、Semaphore、ReentrantReadWriteLock的重要的内部类都主要是在AQS上调用功能,从而达到并发工具的要求。

那AQS的全称是什么呢?AbstractQueuedSynchronizer,中文翻译也就是抽象的队列同步器,专门抽象出来给所有并发工具类使用的一个抽象类。

一、学习AQS的前提

AQS里面的核心是state的控制和LockSupport类的API支持,state我们先不说,等下分析源码的时候我们可以知道state是怎么被控制的以及状态的反转。

LockSupport是什么?和我们用的Lock和Synchronized关键字、Lock有什么区别

上代码

//这行代码会抛出一个异常,因为Lock的执行顺序必须先上锁才可以进行unlock操作。
public void testLock(){
       Lock lock = new ReentrantLock();
        new Thread(()->{
            try {
                Thread.sleep(50000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //在这里进行lock
            lock.lock();
        }, "A").start();

        new Thread(()->{
            lock.unlock();
        },"B").start();
 }  
//然而用LockSupport的时候并没有发生任何错误
public void testLockSupport(){
        Thread A = new Thread(()->{
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //在这里进行lock
            LockSupport.park();
            //LockSupport.park();
            System.out.println("I'm coming");
        }, "A");
        A.start();
	 	
        new Thread(()->{
            LockSupport.unpark(A);
            //我们可以测试一下知道,即使我解锁了两次,一样不影响得到想要的结果
             //LockSupport.unpark(A);
            System.out.println("I'm leaving");
        },"B").start();
}
  

看到park和unpark的简述我们可以知道,**“许可”**是一个仅有一个的通行证,unpark会为它加一,park会为为它减一,但是也只有一个通行证。

在这里插入图片描述
在这里插入图片描述

好了,到这里我们也就大致了解了LockSupport提供了怎么样的支持,接下来一起来看看AQS怎么看源码吧。

二、AQS源码解析

因为ReentrantLock用AQS写,我们可以Debug一下它来看一下他的lock方法和unlock方法是怎么用。

在这里插入图片描述

可以看到在ReentrantLock中有一个抽象类继承了AQS,我们主要看他的非公平锁实现,也就是NonfairSync

重要的数据结构AQS里的Node构造一个双向链表,读者自己去看一下源码的简单解析,再回来探讨。

2.1 acquire()方法详解

   	//获取锁,传入的arg = 1
    public final void acquire(int arg) {
        //这里的大致操作就是
        //1、先尝试去获取锁,获取成功直接返回,不成功跳到2;
        //2、尝试构造一个Node节点加入队列
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //这时进来说明线程没有获取锁成功,并被jvm判断到线程已经中断了,线程再进行自我中断
            selfInterrupt();
    }

2.2 nonfairTryAcquire()方法详解

//调用传进来的参数  acquires是1,即获取一个许可即可
final boolean nonfairTryAcquire(int acquires) {
    		//获取当前线程,为阻塞线程或者允许线程做准备
            final Thread current = Thread.currentThread();
    		//这个状态就是 AQS中的state,在ReentrantLock使用它的语义中,state >= 1时有线程在使用,state = 0时无线程使用
            int c = getState();
    		//通过CAS抢占
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
	                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    		//表示当前占用这个锁的线程又重新入了锁,加状态给他,设置好state值
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
 }

2.3 addWaiter()方法详解

 private Node addWaiter(Node mode) {
     	//为加入队列构造一个新节点,mode为独占模式,即EXCLUSIVE,Node还有一个模式为SHARE,即共享模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
     	//如果链表已经初始化了,直接加入链表的尾,这时的  node.waitStatus = 0,即最初始化的状态,没有任何意义
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
     	//重点方法,他会初始化队列,详情看2.4解析
        enq(node);
     	//这里返回构造进队列的节点,直接开始到方法2.5
        return node;
    }

2.4 enq()方法解析

//同步器的队列为null的时候进来初始化,并让它进队
private Node enq(final Node node) {
    	//自旋,里面有初始化队列的步骤
        for (;;) {
            Node t = tail;
            //我们可以从这里知道的是,如果同步器的队列本身为null,他会创造一个什么都不代表的节点,即哨兵节点作为head。
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //第二步自旋会回到这里,进行尾部的设置,新的线程节点进入双向队列
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

2.5 acquireQueued()方法详解

 final boolean acquireQueued(final Node node, int arg) {
     	//假设失败是true
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋获取锁
            for (;;) {
                //获取该节点的前一个节点,比如  pre->node,这时候的p拿到的就是pre节点
                final Node p = node.predecessor();
                //尝试去获取锁,如果锁空闲
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                     // help GC,这时候把队列刚开始初始化的头结点踢出去,帮助GC,节省内存,换成了下一个节点为头节点,同样也是作为哨兵节点
                    p.next = null;
                    failed = false;
                    return interrupted;
                }
     		   //这里会用的LockSupport的park方法,不要着急,我们慢慢看到2.6、2.7对两个方法的解析就知道是什么样的了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

2.6 shouldParkAfterFailedAcquire()方法解析

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //看前置节点的等待状态是怎么样的
        int ws = pred.waitStatus;
        //前置节点设置了等待唤醒,
        if (ws == Node.SIGNAL)
          	//前置节点设置了等待唤醒,所以这个节点可以安全的进入2.7方法来确定可以调用LockSupport的park方法了
            return true;
        //当ws>0时,可以知道前置节点绝不是在等待唤醒,我们可以跳过这些没用的节点,相当于删除节点了
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //这时找到了pred.waitStatus = 0 并且略过了很多节点
            pred.next = node;
        } else {
           	//这里ws = 0 或者 ws = -2 或者 -3,我们直接把节点设置成Node.SIGNAL,即-1,等待正在占用锁的那个successor线程调用LockSupport锁的释放,在队列的线程正在等待调度。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        //返回失败,证明当前调度的线程还不足以安全的进入park环节,即2.7方法
        return false;
    }

2.7 parkAndCheckInterrupt()方法详解

//来到最底层的一个算法,可以在这里看到,park方法和interrupted的方法是在park方法后使用的一个方法
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
    	//返回线程是否已经中断,并清除中断状态
        return Thread.interrupted();
}

2.8 lock方法小总结

到目前为止,我们已经完全解析完了AQS的lock源码,可以知道AQS的一个情况就是双向队列要么为null,要么就是没啥用的哨兵节点

多线程工具的源码最好的解读方式就是DEBUG用单线程的方式去先抢占锁一段比较长的时间,再调用其他线程去抢锁看他的方法是怎么走的,这是鄙人目前探索到比较好的一种多线程源码方式,相对于理解也比较好。

2.9 unlock之release方法

    //因为整个释放过程是独占线程在操作,基本不存在并发问题,所以特别容易理解
    public final boolean release(int arg) {
        //先去尝试释放锁,里面的大概流程就是  调整AQS的状态state 和 把AQS的独占线程设置为null
        if (tryRelease(arg)) {
            Node h = head;
            //这里没处理的一个情况就是,队列为null,只有一个线程在使用并抢占着这把锁,所以不需要调用LockSupport去唤醒所谓阻塞的线程
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            //返回true,释放成功
            return true;
        }
        //尝试释放失败
        return false;
    }

2.10 unlock之unparkSuccessor()方法

//这是用来唤醒队列中被阻塞的节点的
private void unparkSuccessor(Node node) {
   	//传进来的节点是双向队列的头节点head,即node = head;
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

 	//找到头节点的下一个节点,从lock方法中我们知道,双向队列的头节点必为哨兵节点或者为null
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        //以上两种情况出现,从队列的尾部开始往前遍历,为什么呢?因为lock的时候是从高并发线程节点是从尾部先插入的并指向前面的,等我粘贴一段代码给你看看
        /***	
        		这段代码是在自旋的基础上进行的,只要CAS不成功,它就一直自旋
                node.prev = t;//这个很重要,因为node.prev的意思就是它从尾部指向了前面的节点
                //CAS不一定会成功,但上面这一行代码一定会成功
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
        ***/
        
        //从时间的发生点来看,加入有线程刚好去到t.next = node的之前,刚进if方法,并没有执行这句话,那么我们可以知道t.next 可能是空的,所以这里采取了尾部往前遍历的方式找到一个最前面的状态waitStatus状态为 <= 0且不为头节点的snode节点来进行唤醒。
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //唤醒操作,node结构里有个线程,唤醒该线程就可以了
    if (s != null)
        LockSupport.unpark(s.thread);
}

总结

总的来说,AQS是一个CLH的变种,AQS的特点是双向队列上每个节点都有一个waitStatus状态,获取不到锁的时候,自旋一段时间后阻塞让出时间片(上下文切换需要),等待前驱节点唤醒后驱节点,这就相当于CLH(自旋) + MCS(MCS是在自己的结点的locked域上自旋等待,即自己调用LockSupport.park())了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值