AQS核心思想01
本文为本人基于对AQS的理解所著,也是对所学知识的一种整理,希望能够提炼出AQS设计的核心思想,并且能够借助AQS进行多线程功能类的自定义;
在本文中,为了理论知识的紧凑度,对源代码的引用将尽量减少,一般会指出在哪个类的哪个方法中,具体源码如何实现不如直接打开源码一一查看;
本文纯手打,也是在边学习过程边记录下来的~ 如有问题,欢迎大佬指正!
文章目录
前言
AQS是java juc包的核心类(AbstractQueuedSynchronizer),可以说juc绝大部分的类的底层实现都是基于该类的。
一、独占模式与共享模式:
独占模式:
独占模式核心思想为:在同一时间只有一个线程能拿到锁执行,这种情况拿到的锁称为独占锁。但是需要注意,只是说该锁只能同时被一个线程拥有,但是该线程能拥有多少共享资源,则需要看锁的具体实现了(是否可重入)
独占模式核心加锁方法为:
- acquire()
- tryAcquire()
- acquireQueued()
这三个方法基本能够体现独占模式的核心思想了,其他方法也大致类似~
共享模式:
共享模式核心思想为:在同一时间有多个线程可以拿到锁协同工作,一般根据AQS中的state值来判断是否还存有多余共享资源。
共享模式核心加锁方法为:
- acquireShared()
- tryAcquireShared()
- doAcquireShared()
这三个方法基本能够体现共享模式的核心思想了。如CountDownLatch类就是通过操作这几个或类似命名的核心方法,实现类的功能的。
二、AQS中的队列:
CLH:
CLH锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在前驱节点的本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
相对于CLH锁,还有一种MCS锁;CLH锁一般用于SMP(对称多处理器结构)系统结构中,而MCS锁则一般用于NUMA(非统一内存访问结构)系统结构中。具体这两种结构都是什么,就不一一阐述了。
而由于当前大部分PC都是SMP架构,所以Java JUC包的AQS核心类就是基于CLH锁实现的,因此可以看到很多文章称AQS中的同步队列为CLH队列。
CLH队列(同步队列):根据CLH锁建立的链表就是CLH队列,AQS中的同步队列就是CLH队列;当线程进入 循环–尝试获取锁–阻塞 模式前,首先会被封装为Node结点,并加入到同步队列尾部。当执行获取锁的方法(如doAcquireShared()、acquireQueued())尝试获取资源失败后,将会尝试将线程进行park阻塞,然后等待被唤醒。
因此,在AQS中,CLH并不是通过自旋锁实现的(这样太耗费CPU资源了),而是节点在通过绑定可唤醒自己的前驱节点(waitStatus==signal)后,直接进入阻塞状态,等待前驱节点的唤醒!
具体参考AQS中addWaiter()、enq()、shouldParkAfterFailedAcquire()等方法,都是AQS操作CLH队列的方法,后续有详细的使用流程。
条件队列:
说到条件队列,首先需要看AQS的内部成员类ConditionObject类:该类是Condition的子类,提供了对条件的控制机制。
该类核心方法有:
- await();
- signal();
- signalAll();
ConditionObject通过这三个方法,完成对条件的控制,从而能够有序的推进线程。如CyclicBarrier类就是通过该ConditionObject类完成多个线程的阻塞以及达到指定数量(条件)后的统一唤醒机制的。
当通过ConditionObject类对象调用await()方法时,其内就会调用addConditionWaiter()方法将当前线程封装成Node结点,并加入到条件队列中。同时由于条件控制一般是用于独占模式,因此在因得不到条件而阻塞前,首先要释放掉当前线程所持有的独占锁,然后进入 循环–阻塞–等待条件满足 的过程。
当其他线程调用signal()、signalAll()后,相对于条件被满足,这两个方法内部,就会将条件队列中的结点加入到同步队列中。同时,这些结点内包含的线程就能跳出await()中的等待条件循环(条件被满足),于是调用acquireQueued()方法进入CLH队列的 循环–尝试获取锁–阻塞 模式中了。
可以看到,AQS的条件队列,其实就是附加了一个条件的机制,通过ConditionObject类对象调用await()方法的线程,不能直接尝试获取独占锁,而是会进入阻塞状态以等待条件的到来。而signal()或signalAll()方法就标识着条件已经到来,因此条件队列中的线程就转移到了同步队列CLH中了。
三、AQS的核心方法实现:
1、Node类:
Node类是AQS的静态内部类,是AQS中两个队列实现的基础(AQS的两个队列都是通过链表的形式实现的,并不存在实际的队列实例)。
- 通过prev、next两个对象指针指向同步队列的前驱和后继节点,
通过nextWaiter对象指针指向条件队列; - 通过thread变量存储当前节点封装的线程;
- 通过waitStatus变量记录当前节点的状态;(很重要)
2、ConditionObject类:
ConditionObject类是AQS的成员内部类,前面已经阐述了其核心方法,这里留着做后续补充,敬请期待~
3、核心成员变量:
- state变量:该变量是一个int型变量,是AQS类提供的共享资源,AQS所有的线程同步功能,都是基于该变量 + CAS进行实现的!
- head、tail用于记录同步队列的头、尾节点。
4、核心方法实现:
不管是独占模式还是共享模式的核心方法,以及其核心方法的类似方法,其实都是同一套模板,只是两者有点细微的变化而已。此处拿独占模式前面列举的核心方法举例:
-
acquire()
该方法就是一个指挥者,指挥真正发挥作用的方法的执行,可以说是对外暴露的一个门面方法。 我们查看源码,可以发现核心逻辑就是首先通过tryAcquire()方法 首先进行一次尝试,获取共享资源,如果获取失败,就会调用acquireQueued()方法去获取。 -
tryAcquire()
该方法如果我们查看AQS源码会发现,内部直接抛出异常。也就是说AQS并没有提供具体实现,因为这个方法就是java设计者提供给后续各个功能类实现各自功能的核心方法!我们可以打开一些JUC中的具体功能类,可以发现内部一般都有一个Sync类;这个类就是AQS的实现类,其中一般都要实现本方法。(当然,根据共享/独占模式的不同,实现方法有所不同)具体拉取暂时不在这写了,后续看情况再开文章详细写。
这个方法也是独占模式与共享模式最大的不同点(共享模式的方法名字有所变化):独占模式返回值为boolean类型,而共享模式返回的是int类型。
这细微的变化却反映了独占模式与共享模式的关注重心:
- 独占模式下,要想实现 同一时间只有一个线程能持有锁 的思想,本方法逻辑应该为:只有无锁状态(state==0)或 允许重入且当前线程为持锁线程时才令当前线程持有锁,并返回true,否则返回false。(即重心应该放在对持锁线程的验证上,线程验证通过时为true,否则为false)
众所周知ReentrantLock是独占可重入式锁,该锁的实现原理也就是基于此逻辑实现。
- 共享模式下,要想实现 在同一时间有多个线程可以拿到锁协同工作 的思想,该方法的逻辑应该为:只要当前共享资源(state)的剩余量能够满足当前线程所需要的量(剩余资源量 - 需求资源量 >= 0),就分配资源给当前线程(即给当前线程加共享锁),并返回 [剩余资源量 - 需求资源量]。(即重心应该放在共享资源量的剩余量上,不管当前是什么线程,只要剩余量足够就分配资源(分配共享锁))
- acquireQueued()
该方法是AQS提供的获取锁的具体实现方法,核心思想很简单,就是不断循环–调用tryAcquire()尝试获取锁–(获取锁失败)阻塞等待唤醒以进入下一次循环 的模式直到成功获取锁。当然JUC设计者也为开发者提供了有限尝试的策略 如有限时间内尝试获取等,实现方式大同小异,不再过多阐述。
其他类似方法和共享模式的核心方法都大同小异,不再过多阐述~
通过对三个核心方法的了解,我们可以大致知道AQS的工作原理了,但是这只是提供给我们一种设计思路以及自定义多线程功能类时的流程。但是AQS内部还有很多细节处设计的十分巧妙,等待着我们探索 ~
感谢您的阅读!
该文章为本人纯手打,本人也还在学习过程中 ~ 如有错误,欢迎大佬指正!