AQS全称AbstractQueueSynchronizer,是区别于Synchronized,是java提供的另一个实现同步的体系。
从下图中可以看出,常用的控制并发的工具都是基于AQS实现,比如ReentratLock,读写锁等。
为什么要设计一套AQS框架?
当程序发生并发的时候,有临界区需要保护,保护临界区最常用的就是锁(互斥量Mutex)
锁的一种实现有Synchronized,另一种保护临界区就是信号量:控制临界区的并发量,并发量=1的时候就是互斥量。它们利用底层的类Unsafe,LockSupport,Condition等实现更加灵活,更加适合更多场景的锁。但是对于我们专注于使用锁的开发者来说使用这些底层的类Unsafe,LockSupport对我们来说是相对复杂的,而且要实现一个线程安全的队列,虽然java给了,但是我们要去实现的已经是很底层的东西,如果用它提供的工具,但是性能不好。所以在做底层研发的时候,我们希望能直接对到操作系统上去,比如unsafe.cas,这都是很底层,我们就用这些去构建我们的数据结构和算法,但是java中大量的高端用户他们不去实现这些,他们要用这些东西,就要熟悉这些很多的技巧,比如一致性的保证,cas的loop等,而且这些底层类提供的api很复杂,所以很有必要我们再去提供一套工具,去使得实现我们的数据结构非常的舒服,这套工具就是AQS。AQS对底层类以及队列的操作进行了进一步的封装,我们可以写一个类去继承AQS实现它需要我们自定义的方法。
AQS是区别于Synchronized,是java提供的另一个实现同步的体系。Synchronized通常称为内嵌锁,本身也是c/c++体系monitor和java结合实现,AQS是完全建立在java之上的,所以用起来更方便,不用太考虑底层 。
AQS实现了一个先进先出fifo的等待队列,此类支持默认的独占模式和共享模式(CountDownLatch,可以获得多个资源)之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。ReentrantLock只支持独占模式
简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点。
与synchronized相同的是,这也是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。令人疑惑的是为什么采用CLH队列呢?原生的CLH队列是用于自旋锁,但Doug Lea把其改造为阻塞锁。
基于Node的的CLH阻塞队列是如何运作的
首先 CLH队列锁通常使用自旋锁来阻塞线程执行,使用本节点和前置节点的waitStatus来判断线程是否阻塞。在前置节点获取执行权限的时候发出信号。每个节点都有一个单独等待通知的监视器,waitStatus不会控制线程是否获取到了锁。获取锁的过程是通过查看队列中的第一个node中的waitStatus是否处于可以执行的状态。如果可执行则继续执行,线程被中断或者超时了就寻找后续node.
CLH锁出列只设置更新头部节点,插入队列只需要原子更新尾部的节点。
首先确定自己是否为头部节点,如果是头部节点则直接获取资源开始执行,如果不是则自旋前置节点直到前置节点执行完成状态修改为CANCELLED,然后断开前置节点的链接,获取资源开始执行。
当有线程竞争锁时,该线程会首先尝试获得锁,这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来,与synchronized实现类似,这样会极大提高吞吐量。 如果已经存在Running线程,则新的竞争线程会被追加到队尾,具体是采用基于CAS的Lock-Free算法,因为线程并发对Tail调用CAS可能会导致其他线程CAS失败,解决办法是循环CAS直至成功。
AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(无锁,CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
AQS的acquire方法,尝试获取资源(CAS),如果成功则返回,如果失败则加入fifo的队列中并阻塞。等到前面的节点都出去了,轮到自己的时候再循环尝试获取资源。//整个过程忽略中断
Acquire方法有两层的逻辑:
void acquire() {
while(尝试获取锁,如果失败tryAcquire){
如果队列中没有当前线程: queue.add( currentThread ) ://加入队列
park();//休眠线程
}
}
先有一个while循环不停的尝试获取锁,尝试获取锁这个逻辑(tryAquire方法)怎么是获取成功,怎么是失败是这个逻辑是控制反转的,意思就是用户自己提供的,如果失败,就放如clh队列,然后休眠park()。
AQS的release方法,尝试释放资源,然后再唤醒等待队列中的下一个节点。
aqs实现的公平锁和非公平锁的最主要的区别是:非公平锁中,那些尝试获取锁且尚未进入等待队列的线程会和等待队列head结点的线程发生竞争。公平锁中,在获取锁时,增加了isFirst(current)判断,当且仅当,等待队列为空或当前线程是等待队列的头结点时,才可尝试获取锁。
synchronized和ReentrantLock的区别
1 都是做临界区保护的(提供锁/解锁能力)
2 都是可重入的(重入的好处就是可以lock很多次,最大的好处是程序员不用担心lock一次之后就必须立即得unlock,原理就是进来的线程和拥有锁的owner线程作比较,如果相同,就可以直接拿到锁,避免了很多锁的竞争)
3 都提供线程间协作
4 提供锁的升级逻辑,synchronized是基于monitor的锁升级逻辑,reentarntlock 基于aqs,cas竞争->休眠+排队竞争
5 都提供等待队列 syn:EntrySet,waitSet reen:CLH队列
不同点在于:、
1.ReentrantLock基于AQS实现,Synchronized基于Monitor实现
2.ReentrantLock的锁是可响应中断的
3.ReentrantLock提供获取锁超时的tryLock方法,能够通过参数设置等待的时间。
4.ReentrantLock可以跨block加锁
5.ReentrantLock可以配置锁的公平性
6.条件变量,synchronized也有条件变量,指的就是不满足条件时进入waitSet等待。ReentrantLock相比于synchronized来说,它可以有多个条件变量,即可以有多个waitSet。通过lock.newCondition()来创建Condition对象,每一个Condition就相当于一个waitSet,不满足这个条件的就进入这个WaitSet。await/signal(all)来进入等待和唤醒一个waitSet中的某一个线程或者全部线程。