AQS 是什么? 由浅到深 源码分析。

一 ,概述

AQS 全名 AbstractQueuedSynchronizer 是juc 包下的一个 抽象类 也叫抽象队列同步器 是用来实现 同步器(也就是锁)的一个 解决方案, 平时开发中用到的juc下的那些东西比如ReentrantLock ,CountDownLatch 还有个读写锁的那种 都是通过实现AQS 来实现的 。

二, 原理

线程执行lock 方法的时候呢 会通过cas 方法 替换一个 状态位 如果替换成功那就 拿锁成功, 执行代码。如果替换失败(队列同步器
队列同步器 那玩意说白了肯定是有个队列的) 进入到队列里面 然后 直接把线程挂起。 等着吧 等着拿到锁的线程释放锁的时候 从队列里拿到
等待的线程 然后唤醒就ok。 嗯大概就这样。 下面看下这些要素在源码里对应的属性把。

AbstractQueuedSynchronizer 类中重要的属性

//锁状态位  这个就是上面说的 那个cas 替换的东西。
private volatile int state;

//头节点
private transient volatile Node head;

//未节点
private transient volatile Node tail;

// 当前持有锁的线程。 这个是aqs 的父类的属性。
private transient Thread exclusiveOwnerThread;

说下head 也就是队列的第一个节点 是一个虚拟节点 里面是没有线程信息的。他的下一个节点 才会使正常的排队的线程node。

为啥用虚拟节点? emmm… 其实不用这个虚拟节点也可以的。我觉得是为了屏蔽 真正节点的各种差异,,因为涉及到一些唤醒啊啥的操作 那种真正的节点 是否向后唤醒 会有好多判断啥的 判断起来特别麻烦。索性 抽出来也给head 节点 就好了。 个人感觉 大家也可以想想为啥哈哈

ps: 这里的变量都用到了volatile 修饰 为了保证可见性使用的。关于volatile 如果不了解可以去看下 我的这个文章嘻嘻小小支持一下

Node 结构

head 和 tail 的 类型是Node 这个Node 是 AQS 抽象类中的一个 内部类 他的结构就是一个 双向链表 。 这玩意东西挺多的眼花缭乱 刚开始看没啥用 反而直接劝退了。来瞅一眼精简版的Node 嘻嘻

 static final class Node {
        
        //节点 状态 (这个在 唤醒节点等时候有用 不急)
        volatile int waitStatus;
		
		//上一个节点
        volatile Node prev;
        
        //下一个节点
        volatile Node next;
        
        //阻塞的线程对象
       	volatile Thread thread;

}

image.png

线程挂起

LockSupport.park(Thread thread)

线程唤醒

LockSupport.unpark(Thread thread);

cas 替换

Unsafe 类 里面的方法

ok 相信大家看到这 不知道大家有没有这种想法 哎 差不多了 这aqs 也不过如此!!! 我觉得自己琢磨琢磨就能自己写个aqs了 哈哈哈 。 其实真正实现起来 里面细节可是太多了。哈哈 慢慢往下看 下面再将就得结合 具体的实现才能讲的清楚了 。

三,源码分析

1 ReentrantLock

手撕ReentrantLock 直接就是它 不解释。

ReentrantLock 它包含了 重入锁, 公平锁, 非公平锁 , 还有超时中断等功能。 这玩意一听就挺复杂啊··这是我能触及到的东西嘛。
在这里插入图片描述
慢慢来 先看下 asq 在reentrantLock 中是咋用的

囊 他没有 直接实现aqs 他也先整了个抽象类 Sync 去继承了 aqs
在这里插入图片描述

嗯 然后基于 公平非公平的 类型 去 整了两个Sync 的实现类

NonfairSync
在这里插入图片描述
FairSync
在这里插入图片描述
两个公平非公平的实现都有了 我怎么控制它啥时候用公平锁 啥时候用非公平锁啊 ok 看它的构造就知道了。
在这里插入图片描述
传true or false 想用哪个用哪个 啥都不传默认非公平。aqs 的state 代表持有锁线程的重入次数。 0 代表 没有锁持有 线程
好 行了 收 开始了。

lock() 方法

直接.lock() ctrl 点进去
在这里插入图片描述
emmm… 混子 不干实事。 具体执行在 sync 里面 sync .lock() 有两种不同的实现 一个是公平锁 一个是非公平锁的。 不慌 一个都跑不了。

我们可以先想一下 公平锁和非公平锁 的区别是啥 公平锁 是按顺序来的 先来后到 得先看下有没有人排队 如果没人排队才能枪锁。有人排队就不行 。 非公平锁 不管三七二十一 我先抢一手再说。

 // 非公平锁
 final void lock() {
 	//进来 我直接先抢一手。 就是通过cas 去更改 aqs 中的state 状态位改成1 这是说下状态0 代表 没有人持有锁 
    if (compareAndSetState(0, 1))
    	//如果我抢到锁 我直接设置 当前线程到 aqs 的 exclusiveOwnerThread 上面 代表当前持有锁的线程 (这个不用进队列 只有抢不到锁的 才会进入队列 抢到锁的不用进队列哦)
       setExclusiveOwnerThread(Thread.currentThread());
     else
       //如果抢不到 执行这个方法。
       acquire(1);
 }
//公平锁
 final void lock() {
     acquire(1);
 }

因为公平锁和非公平锁 都会 走这个 acquire 方法
非公平锁 先枪锁。 抢锁失败走这个方法。
公平锁 直接走这个方法。

这里思考一下 acquire 方法里面会干什么事 - -

公平锁应该会先判断下 state 状态吧 如果是0 看下队列前面有没有人排队。如果有人排队 加入到 队列里面。 如果没人排队
直接抢锁就好了。 非公平锁既然也会走这个方法。他是抢锁失败进入的 那他应该会直接去排队吧 哈哈。

ok 猜完了 直接看源码。

acquire

AbstractQueuedSynchronizer->acquire(int arg)

  public final void acquire(int arg) {
      	// 我直接剧透
      	 //再次获取锁如果获取失败 走下面    
        if (!tryAcquire(arg) &&
        	//先执行addWaiter构建Node节点放入队列 再执行acquireQueued  for 死等 (挂起线程 等待执行)
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //理论上 不会进入这个if 里面 
            //这个方法是设置线程中断标识位的 没啥用先忽略
            selfInterrupt();
    }
tryAcquire

AbstractQueuedSynchronizer-> tryAcquire(int arg)

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

aqs 里这个方法直接抛异常了 ,意思是 如果要使用 acquire 那子类必须要实现这个方法咯

so ReentrantLock 里面实现了这个方法 。 公平锁和非公平锁 有不同的实现。

  //公平锁
 	 protected final boolean tryAcquire(int acquires) {
 	 		//获取当前线程
            final Thread current = Thread.currentThread();
            //获取aqs 状态为 state
            int c = getState();
            // == 0 说明 没有线程持有锁  
            if (c == 0) {
            	//hasQueuedPredecessors 解读 这个代码块的最下面
                if (!hasQueuedPredecessors() &&
                	//cas 抢锁
                    compareAndSetState(0, acquires)) {
                    //抢锁成功 把当前线程设置成持有锁的线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果上面判断失败 进入这里 看下是不是锁重入 如果是 state +1 return true
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //都没走return false
            return false;
        }

//非公平锁  这个我就不说了大家自己猜下把哈哈哈
	final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            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;
        }

//========================================================================================================
	//判断一下子 我前面是不是还有人 true 有人 false 没有
    public final boolean hasQueuedPredecessors() {
      
        //获取尾节点 和头结点
        Node t = tail; 
        Node h = head;
        Node s;
        //先比较 头尾节点  如果相等。 说明 只有一个节点 并且是虚拟节点没有排队的 直接返回false  
        return h != t &&
        // 头节点的下一个节点 不是 null 并且 是当前线程 返回false
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

说白了 tryAcquire 方法就是抢锁的。 公平锁和非公平锁的区别就是 公平锁抢锁之前要看下是否是 排在队列的第一位通过hasQueuedPredecessors() 这个方法。

addWaiter

AbstractQueuedSynchronizer-> addWaiter(Node mode)

    private Node addWaiter(Node mode) {
		//以当前线程 构建 Node 节点
        Node node = new Node(Thread.currentThread(), mode);
  		//获取aqs 中的尾节点
        Node pred = tail;
        if (pred != null) {
        	//把当前线程所属节点的 prev(上个节点) 指向 当前队列的最后一个节点 
            node.prev = pred;
            // 然后 cas 替换 把 tail 指向 当前线程所属节点。
            if (compareAndSetTail(pred, node)) {
            	//cas 成功 把 当前线程节点的 prev 的 next (下个节点 指向当前线程节点 )
                pred.next = node;
				//ok 了 说明加入队列成功了 直接返回当前node
                return node;
            }
        }
        // 如果说 tail == null  (tail == null 说明 队列还没初始化完成) 或者是 cas 替换失败 。怎么办 猜一下。  没抢到锁 他是一定要加入队列的啊·· 所以他肯定是 要放进去的 咋放 死循环 哐哐硬放。 
        enq(node);
        return node;
    }


//===============================================================================

  private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //如果 tail 是null
            if (t == null) { 
            //ok 了这里就是 这个队列的初始化操作了。 说虚拟节点就是在这里创建的。
            //创建一个空节点通过cas 设置成 head (如果cas 失败说明有其他线程 也在初始化这个 队列 并且 设置head 的操作被别的线程抢到了 那我直接跳过 进行下一次循环就ok )
                if (compareAndSetHead(new Node()))
                	//再设置 tail 节点为 head 节点。
                    tail = head;
            } else {
            	// t 不是null 和addWaiter 时候一个操作 比较并替换 把现在的为节点 替换成 当前节点。 
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                	//cas 成功 把 当前线程节点的 prev 的 next (下个节点 指向当前线程节点 )
                    t.next = node;
                    return t;
                }
            }
        }
    }

大概意思:前面抢锁失败 构建Node 节点 塞到 queue 里面排队

如果上面tryAcquire 方法返回false 的话才会进入这个方法 至于参数 Node.EXCLUSIVE 不用管 这个是 用来标识 是共享锁还是排他锁的。我们这个都是排他锁

入队流程

1 先把当前 线程所属node 节点的 prev 设置成  当前队列的最后一个节点。 
2 然后通过 cas 把 tail 替换成当前node 所属节点 
3 最后 把 当前node 节点的 prev 节点的 next 属性设置成 当前node 节点。形成双向链表 大功告成。

思考 : 我们看源码的时候 相信大家也发现了 他只在设置 tail 节点的时候使用了 cas 操作 最后的 替换 prev 的next 的时候没有使用cas。 所以目前看 尾节点的插入只保证了 从后向前遍历链表的安全性, 并发情况下 从前向后遍历链表 是会有问题的(比如我cas 替换成功 说明我已经加入链表了, 但是还没有走到 第三步。 那 其他线程 去从head 向后 遍历 prev 的next节点就是null, 这个时候就丢节点了。 emm 这个问题具体体现再哪里 cas 又是怎么解决的 ?)

acquireQueued

ok 把 当前线程所属Node 加入到 队列中之后 就到了 acquireQueued 方法。目前我们还没看到线程挂起的操作在哪里 那肯定就是再 acquireQueued 里面了。

AbstractQueuedSynchronizer->acquireQueued(final Node node, int arg) ;

    final boolean acquireQueued(final Node node, int arg) {
        //这是初始化了一个 标记为 标识 是否失败。默认true
        boolean failed = true;
        try {
        	//线程中断标记为 默认false
            boolean interrupted = false;
           	//又是一个死循环。   这个比较简单了 如果要走出这个死循环让线程继续执行 那真相只有一个 抢到锁 return 。
            for (;;) {
            	//获取当前节点的上个节点 为啥不是 node.prev 这个 你点进去看下就知道了。。。不过是加了个校验而已。
                final Node p = node.predecessor();
                //如果你的上个节点是 头节点。 并且我通过 tryAcquire 拿到了锁 
                if (p == head && tryAcquire(arg)) {
                //ok 那我终于可以执行了。 但是这个执行 就需要有些条件了 因为你要把 队列的节点 删掉呵呵。
                	// 变相删除 当前节点 再当前代码块的下面
                    setHead(node);
                    //方便gc具体可以自己去看下 jvm 垃圾回收算法哈哈 这里不过多赘述了
                    p.next = null; 
                    //设置是否失败 为false 
                    failed = false;
                    //返回线程中断标记位
                    return interrupted;
                }
                //如果没资格竞争锁资源 。 那就需要挂起了 这两个方法放在下面单独说下
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        //如果线程中断标记为是 true  走这个 目前来说 不会出现这个情况的parkAndCheckInterrupt 方法 返回的一直是false (除非你自己去变更线程的中断标识- - )
            if (failed)
                cancelAcquire(node);
        }
    }


//================================================
	
    private void setHead(Node node) {
    	// 把 head 指向当前node (不存在线程竞争问题了所以不需要cas)
        head = node;
        //把node 的 thread 设置 null
        node.thread = null;
        //node 的 prev 设置 null
        node.prev = null;
    	//哈哈 其实就是通过把当前节点的整成 虚拟节点 把原来的虚拟节点去掉 变相的相当于删除 当前node 节点了 
    }


当抢锁线程 掉用 lock 方法 如果没有抢到 就会再这里挂起线程 等待唤醒 ,唤醒之后继续抢锁。抢不到继续挂起 知道抢到锁 才会继续执行lock 之后的代码。

shouldParkAfterFailedAcquire

AbstractQueuedSynchronizer->shouldParkAfterFailedAcquire(Node pred, Node node)

这个方法很重要 我们之前说的node 节点的状态位 waitStatus 变化 目前来说 都集中再这里!! 这里回顾下 我们目前 了解到的节点 的初始 状态位。

1 头节点(虚拟节点)

head 在队列被创建的时候初始化的 它直接new Node() 出来的 构造里面也没有对 waitStatus 做任何操作 所以 它默认值是0

在 排队的线程拿到锁的时候也会被初始化 通过那个setHead 方法嘛 那个方法 里面头节点 用的是 拿到锁的线程节点的 waitStatus 值

2 普通节点

目前的普通节点的创建是 在addWaiter 里 通过 Node node = new
Node(Thread.currentThread(), mode); 创建的 默认值也是0

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    	//获取当前节点 的上个节点的 状态位 
        int ws = pred.waitStatus;
        //如果是 -1 直接return ture 回去。 
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
        	//如果大于0 目前来说昂 是没有大于0 的情况的
        	// 在这个逻辑里 看样子 大于0 的节点 属于是被取消的节点。 会被直接从队列删除掉  一个循环 从后向前 把找到的第一个 不大于0 的节点作为 当前线程节点的上个节点。  
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        // 那就是  不是 -1 并且 <=0 的情况了
        	//这种情况 就通过cas 直接改为 -1 就ok
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

注意点
1 这个方法 对状态的变更 只针对 当前节点的上一个节点
2 只有当前节点的上一个节点 的waitStatus 是 -1 才会返回true
3 如果上个节点状态不对(>0) 会把上个节点从队列删除 并重新设置当前节点的prev
so node节点 的waitStatus = -1 代表它的下个节点是 可休眠状态。

总结

这个方法说白了 清空 当前节点到上个可用节点之间的废弃/取消节点, 并更新 当前节点上个节点的waitStatus 为 -1。

parkAndCheckInterrupt

AbstractQueuedSynchronizer->parkAndCheckInterrupt

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

如果上面的 shouldParkAfterFailedAcquire 方法 返回 true 则执行线程挂起。

unlock() 方法

不管是公平锁还是非公平锁 释放锁 都用的同一个方法

	
	
    public void unlock() {
        sync.release(1);
    }
	
	// aqs 里面的 release
    public final boolean release(int arg) {
    	//判断 锁是否释放干净  可重入锁 加锁次数要和 释放次数一致才算真正释放锁
        if (tryRelease(arg)) {
        	//如果释放干净 那就得准备唤醒下一个节点了
        	//获取头节点
            Node h = head;
            // 目前情况 头节点不可能是null (我到现在都搞不到 啥情况 走到这一步的时候头节点回事null)
            //并且头节点的 状态 不是0   0 其实就代表 节点后没有需要唤醒的线程。 为啥 如果你后面真有节点 那在 shouldParkAfterFailedAcquire 方法里  就会把你的状态变为 -1 所以要么你是0 后面没有节点 要么是-1 后面有节点(目前情况来看)
            if (h != null && h.waitStatus != 0)
            	//如果后续有节点 唤醒后续节点
                unparkSuccessor(h);
             
            return true;
        }
        return false;
    }

tryRelease

ReentrantLock-.Sync->tryRelease

        protected final boolean tryRelease(int releases) {
			//获取 aqs 的state   - 1  
            int c = getState() - releases;
            //判断下 释放锁的线程是否是持有锁的线程 如果不是抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果 state - 1 后是0 说明释放干净了 
            if (c == 0) {
            	//设置返回值
                free = true;
                //设置持有锁的线程 为null
                setExclusiveOwnerThread(null);
            }
            //把 -- 后的state 设置回去
            setState(c);
            return free;
        }
unparkSuccessor

AbstractQueuedSynchronizer->unparkSuccessor(Node node)

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        //如果头节点状态<0  把他设置成0 
        // emm 这个操作 我觉得是为了防止重复唤醒。
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

       //获取头节点的下一个节点。
        Node s = node.next;
		//如果下一个节点为null  || 下个节点节点状态 > 0 也就是取消状态
        if (s == null || s.waitStatus > 0) {
		//从后向前遍历 拿到 最靠近head 的节点
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //ok 唤醒。
        if (s != null)
            LockSupport.unpark(s.thread);
    }

ps: OK OK 先到这里吧= = 困了 其实还有tryLock 和 带参数的tryLock 等方法 还有读写锁的那种。 我都还没说。 因为写博客真实太麻烦了··哈哈哈哈so 下次一定哈哈 后续我会更新的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值