现在提到并发应该都不陌生吧,他就是解决单体架构中在多线程下面数据一致性的问题,而我们会很自然地想到JUC下面的类。今天我们就来探究一下AQS底层源码!
技术思路:
并发情况下关键是要解决多线程并行到串行,我们可以通过以下几点来解决:
1.如何实现互斥(排他锁)
2.如果当前已经有一个线程获得了锁,那么没有抢到锁的线程如何处理(数据结构)
3.阻塞存储的线程
4.唤醒队列中的线程(释放CPU资源)
针对上述四个需求,如何实现
1.如何实现互斥,共享资源。
伪代码展示:
int state=0;
抢占锁,state=state+1。
释放锁,state=state-1.
if(stat==0){
//无锁状态,可以去抢占锁
}else{
//有锁状态,那么需要加入到队列中去排队,
}
2、将没有抢到锁的线程存储起来:
addWaiter()的作用是将没有抢占到锁的线程都追加到双向链表后面 (检查状态的时候会有插队现象,往队列里面追加的时候没有插队现象)
把当前没有抢到锁的线程封装成一个节点,然后把尾节点赋值给pred,如果前一个节点不为null,将当前节点作为next节点,再进行CAS操作把当前节点设置为尾节点。当然第一次进来的时候pred是null的。
这是一个死循环,首先把尾节点赋值给局部变量t,如果尾节点是null,就通过CAS操作初始化一个新的节点作为头节点,然后头尾相等,如果不为null的话,这里面的逻辑跟上面是一样的,这里就不重复赘述了。
这里演示存储节点的过程:
第一个线程抢占成功
它就会去执行逻辑了,自然不在这个阻塞链表里面
第二个线程
第三个线程:
第四个线程
3、阻塞存储的线程
(根据节点状态判断当前追加的节点从尾部开始寻找需要追加到哪个合适的节点后面)
这里也是一个死循环,首先获取将要被阻塞节点的前置节点,然后判断如果前置节点就是头节点并且再次尝试修改值成功的话,就把当前节点设置为头结点。否则就从尾到头查找第一个遇到waitStatus为-1需要被唤醒的节点后面。(检查状态的时候会有插队现象(生活中饭堂打饭的场景))
这里把当前节点的线程置为null是因为被唤醒之后,节点里面的线程就没作用了
因为所有线程初始的waitStatus都是0,经过这个方法之后,当前节点之后所有节点的waitStatus的都变为了-1需要被唤醒的状态,其中waitStatus状态有如下几种情况:
①、CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
②、SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
③、CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
④、PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
⑤、0状态:值为0,代表初始化状态。
这里就是真正的阻塞线程了
4、释放锁
这里为什么不是从前往后找waitStatus小于0的节点呢,而是从后往前找?
从前往后找到了第一个放弃了,同样的我可能认为它后一个也是放弃的,所以干脆从后往前