整体介绍
概念
首先阅读一下类的源码注释,可以知道,这几个接口是最关键的。
这几个方法是使用AQS类的关键,只有这几个方法是可以定制的,其他方法几乎都是final的,不可修改。
从代码实现上看,能看到的变量几乎都是volatile的,能看到的方法几乎都是CAS或者Unsafe类的原子方法。
接下来我们来整理一下这个类的字段和方法,这里我们主要关注private字段和public方法。
字段
volatile int state:同步状态。
volatile Node head:等待队列的头,延迟初始化。
volatile Node tail:等待队列的尾部。初始化后,仅通过casTail修改。
方法
获取锁:
acquire(int arg)
acquireShared(int arg)
释放锁:
release(int arg)
releaseShared(int arg)
等待队列:
hasQueuedThreads()
getFirstQueuedThread()
isQueued(Thread thread)
getQueuedThreads()
getExclusiveQueuedThreads()
getSharedQueuedThreads()
条件对象ConditionObject:
await()
signal()
架构
AQS整体结构,包括加锁/释放锁,条件对象await/signal。
功能说明
锁
数据结构
锁的数据结构
一个状态字段state表示同步状态,0表示没有锁竞争,1表示有锁竞争。
head跟tail分别是等待队列的头节点和尾节点,该等待队列是用双向链表实现的。
节点的数据结构
prev:前驱节点
next:后继节点
waiter:等待锁的线程
status:节点状态
节点
节点状态其实有4种:
取消状态:status<0;
休眠状态:status=0;
等待状态:status=1;
条件等待:status=2;
功能实现
- 锁类型
- 独占锁
- 公平锁
- 非公平锁
- 共享锁
- 公平锁
- 非公平锁
独占锁acquire(int arg)
以独占模式获取,忽略中断。通过调用至少一次{@link#tryAcquire}来实现,并在成功时返回。否则线程将排队,可能会重复阻塞和取消阻塞,调用{@link#tryAcquire},直到成功。此方法可用于实现方法{@link Lock#Lock}。
独占锁实现过程
1.tryAcquire
尝试以独占模式获取。此方法应查询对象的状态是否允许以独占模式获取对象,如果允许,则应获取对象。
执行acquire的线程总是调用此方法。如果此方法报告失败,acquire方法可能会将线程排队(如果尚未排队),直到其他线程发出释放信号。这可以用来实现方法{@link Lock#tryLock()}。
这是一个protected方法,在ReentrantLock里实现了FairSync和NonfairSync,也就是公平锁和非公平锁。
1-1.FairSync in ReentrantLock
判断队列中有没有其他线程在等待锁,或者当前线程是第一个在等待锁的线程,也就是等待队列中第一个线程,然后CAS尝试修改锁状态,设置当前线程为锁拥有者。
1-2.NonfairSync in ReentrantLock
不去判断当前线程是否是等待队列中的第一个线程,直接CAS尝试获取锁。
2.acquire
在tryAcquire获取锁失败后,acquire主要做的其实是将当前线程放入等待队列中,然后在循环中判断是否可以参与锁争抢。
由于等待队列优先调度第一个节点进行锁争抢,这里要根据当前线程是否是等待队列中的第一个节点分情况讨论:
当前线程在等待队列中不存在;
当前线程是等待队列中第一个节点;
当前线程不是等待队列中第一个节点;
如果当前节点已存在,且不是头节点,清理队列中无效节点,并且节点继续等待;
如果当前节点已存在,且是头节点,尝试再次CAS加锁;如果加锁成功,并且是共享锁,唤醒后继节点参与锁争抢;如果加锁失败,该节点进入休眠状态,设置一个短暂的休眠期,休眠期内无法被唤醒参与锁争抢;
如果当前节点不存在,创建节点,放入等待队列尾部,进行排队等待锁争抢。
如果被中断,或者在超时时间内没有获取到锁,则加锁失败,从等待队列中清除。
共享锁acquireShared(int arg)
在共享模式下获取,忽略中断。通过至少调用一次{@link#tryAcquireShared}来实现,并在成功时返回。否则线程将排队,可能会重复阻塞和取消阻塞,调用{@link#tryAcquireShared},直到成功。
共享锁跟独占锁最大的区别在于,共享锁可以设置并发数。共享锁status表示可以同时争抢锁的线程数,也就是并发数>1,独占锁并发数=1。
共享锁实现过程
1.tryAcquireShared
这是一个protected方法,在Semaphore里实现了FairSync和NonfairSync,也就是公平锁和非公平锁。
1-1.FairSync in Semaphore
如果有前驱节点在等待,返回失败;
如果剩余的并发数<0,返回失败;
1-2.NonfairSync in Semaphore
不去判断是否有前驱节点在等待,直接根据剩余的并发数来判断。
2.acquire
在独占锁中分析过,这里不再赘述。不同点在于共享锁允许多个线程获取锁。
其实到这里锁的基本功能已经差不多了,如果只是实现线程之间的互斥,那么只需要用到加锁/释放锁就够了。但是如果在互斥的基础上还要进行线程间的协作,就要用到条件对象了。
条件对象ConditionObject
数据结构
条件对象的主要数据结构就是一个条件队列,用于存在调用await方法释放锁后的线程。
功能实现
await方法说明:
将当前线程放入条件队列,然后释放锁,唤醒等待队列中的线程参与加锁。
signal方法说明:
将等待时间最长的线程(如果存在)从该条件的等待队列移动到拥有锁的等待队列。
最后,我想说的是,AQS的实现思想并不仅仅局限于在读写锁中使用,在很多Java中间件、JVM以及操作系统中都有运用,包括其他语言中也有运用。因为这是一个通用的问题,大家都会遇到并且需要解决。我们日常业务开发中也会遇到,只不过需要使用分布式版本的解决方案。