一、AQS--锁的底层支持
1.AQS是什么
- AQS是AbstractQueuedSychronizer的简称,即抽象同步队列的简称,这是实现同步器的重要组件,是一个抽象类,虽然在实际工作中很烧用到它,但是了解它的内部原理是很有必要的,并法包中锁的底层就是使用该抽象类实现的,下面类图
2.分析AQS类
- AQS是一个双向队列,head和tail变量类型是Node类型,分别用于表示队列的队首和队尾
- 在AQS中维护一个单一的状态信息state,可以通过getState,setState、compareAndSetState函数来修改其值。
- 对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的程序可重入次数;对于semaphore来说,state用来表示当前可用信号的个数;对于CountDownlatch来说,state用来表示计算器当前的值。
- 对于AQS来说,线程同步最关键的就是对state的操作,根据state是否属于同一个线程,操作state的方式分为独占式和共享方式
- 在独占方式下,获取和释放线程的方法为:void acquire(int arg) void acquireInterruptilbly(int arg) boolean release(int arg)
- 在共享方式下,获取和释放线程的方法为:void acquireShared(int arg) void acquireSharedInterruptibly(int arg) boolean releaseShared(int arg)
- 使用独占式获取资源是与具体线程绑定的,一个线程获取到了资源,就会标记这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前资源不是自己持有的,就会在获取失败之后被阻塞。比如独占锁ReentrantLock的实现,在AQS内部首先会使用CAS操作把state从0变成1,然后设置当前锁的持有者是当前线程,当线程再次获取锁时发现锁的持有者就是自己,则会把状态值从1变成2,也就是设置可重入次数,而当另外一个线程获取锁的时候发现自己不是锁的持有者,就会被放入AQS阻塞队列之中。
- 对应共享方式的资源是与具体线程不相关的,当多个线程使用CAS操作去竞争资源的时候,当一个线程获取到了资源,另外一个资源只需要使用CAS操作获取即可。例如:Semaphore信号量,当一个线程通过acquire获取信号量的时候,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入到阻塞队列中,如果满足则通过CAS操作获取信号量,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足就会通过自旋CAS获取信号量
3.分析Node内部类
- 变量thread是一个Thread类型,用于存放进入AQS队列的线程
- SHARED用来标记该线程是获取共享资源的时候被阻塞挂起后放入AQS队列的;
- EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;
- waitStatus是用来记录线程等待状态的,可以为CANCELLED(线程被取消了),SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放资源的时候需要通知其他节点)
- prev和next分别代表当前节点的前驱节点和后置节点。
4.AQS内部ConditionObject
- 这个类是用来实现线程同步的,ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列,ConditonObject是条件变量,每个条件变量对应一个条件队列(单向链表列队),其用来存放调用条件变量的await方法后被阻塞的线程,如类图所示,这个条件队列的头尾元素分别为firstWaiter和lastWaiter。
5.独占方式下,获取与释放资源是如何及逆行的
- 当一个线程调用acquire(int arg)获取资源的时候,会首先使用tryAcquire「尝试」获取资源,具体就是设置state值,成功则直接返回,失败则会当前线程封装为Node.EXCLUSIVE的Node节点,插入到AQS阻塞队列的队尾,并且调用LockSupport.park(this)方法挂起自己
public final void acquire(int arg) {
if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg)) {
selfInterrupt();
}
}
- 当一个线程调用release(int arg)方法的时候,会尝试使用tryRelease操作来释放资源,也就是设置state的值,然后调用LockSupport.unpark(thread)方法激活在AQS队列被阻塞的头部的一个线程。被激活的线程,然后使用tryAcquire尝试,看当前状态值state值是否满足自己的需要,如果满足,则激活线程,继续向下运行,否则还是会被放回AQS队列中,然后被挂起
public final boolean release(int arg) {
if(tryRelease(arg)) {
Node h = head;
if(head != null && head.waitStatus != 0) {
unparkSuccessor(h);
}
return true;
}
return false;
}
- AQS中的tryAcquire和tryRelease方法是没有提供具体实现的,需要程序员自行在子类中进行定义,它们的实现就是使用CAS算法对state进行修改,成功返回true,失败返回false,子类还需要定义当调用acquire和release方法时state状态值的增减分别代表什么含义。
- 比如继承自AQS中的独占锁ReentrantLock,定义当status为0的时候表示锁空闲,1表示该锁正在占用,重写tryAcquire方法,就是使用CAS算法,查看state是否为0,如果为0,那么置为1,并且返回true,否则,返回false;独占锁在实现release的时候,在内部使用CAS算法把当前state的值从1修改为0,并且设置当前线程的持有者为null,然后返回true,如果CAS失败,那么返回false
6.共享方式下,获取与释放资源是如何及逆行的
- 当一个线程调用acquireShared(int arg)获取资源的时候,会首先使用tryAcquireShared「尝试」获取资源,具体就是设置state值,成功则直接返回,失败则会当前线程封装为Node.SHARED的Node节点,插入到AQS阻塞队列的队尾,并且调用LockSupport.park(this)方法挂起自己
public final void acquireShared(int arg) {
if(tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}
- 当一个线程调用releaseShared(int arg)方法的时候,会尝试使用tryRelease操作来释放资源,也就是设置state的值,然后调用LockSupport.unpark(thread)方法激活在AQS队列被阻塞的头部的一个线程。被激活的线程,然后使用tryAcquire尝试,看当前状态值state值是否满足自己的需要,如果满足,则激活线程,继续向下运行,否则还是会被放回AQS队列中,然后被挂起
public final boolean releaseShared(int arg) {
if(tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
- AQS类并没有提供可用的tryAcquireShared和tryReleaseShared方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquireShared和tryReleaseShared需要由具体的子类进行实现,子类在实现tryAcquiredShared和tryReleaseShared时要根据具体场景使用CAS算法尝试修改state状态变量,成功则返回true,否则返回false
- 比如继承自AQS实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryAcquireShared时,首先查看写锁是否被其他线程持有,如果是则直接返回false,否则使用CAS递增的state的高16位(在ReentrantReadWriteLock中,state的高16位为获取读锁的次数)
- 比如继承自AQS实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryReleaseShared时,在内部需要使用CAS算法把当前state的值高16位减1,然后返回true,如果CAS失败那么返回false
- 基于AQS实现的锁除了需要重写上述这些方法之外,还需要重写isHeldExclusively方法,来判断锁是被当前线程占用还是被共享
7.另外对与独占方式下void acquire(int arg)和void acquireInterruptibly(int arg),与共享方式下void acquireShared(int arg)和void acquireSharedInterruptibly(int arg)之间有一个单词Interruptibly的区别是什么
- 不带interruptibly的方法意思就是不对中断进行响应,比如线程在调用了不带Interruptibly的方法获取资源或者获取资源失败被挂起的时候,其他线程中断了该线程,那么该线程不会因为被中断而抛出异常,它还是继续获取资源或者被挂起,也就是不对中断进行响应,忽略中断
- 带有该单词的方法要对中断进行响应,也就是线程在调用了带有该单词的方法获取资源获取获取资源失败被挂起的时候,其他线程中断了该线程,该线程就会抛出InterruptException异常而返回
二、源码:
- 所在包:com.ruigege.ConcurrentListSouceCodeAnalysis5
https://github.com/ruigege66/ConcurrentJava
- 欢迎关注微信公众号:傅里叶变换,个人账号,仅用于技术交流