AbstractQueuedSynchronizer类研究
1.简介
java队列同步器(AbstractQueuedSynchronizer简称AQS)是用来构建锁或者其他同步组件的基础框架,它使用了一个内置的int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这就是需要使用同步器提供的3个方法:getState()、setState(int)和compareAndSetState(int, int)来进行操作,因为它们能够保证状态的改变是安全的
2.类图及方法
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态:
getState():获取当前同步状态;
setState(int):设置当前同步状态;
compareAndSetState(int, int):使用CAS来设置当前状态,该方法能够保证状态设置的原子性。
2.1子类需要重写的方法
方法名称 描述
(1) protected boolean tryAcquire(int arg) 独占式地获取同步状态,实现该方法需要查询当前状态并判断当前状态是否符合预期,然后再进行CAS设置同步状态
(2) protected boolean tryRelease(int arg) 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
(3) protected int tryAcquireShared(int arg) 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
(4) protected boolean tryReleaseShared(int arg)共享式释放同步状态
(5) protected boolean isHeldExclusively() 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
2.2同步器提供的基础方法
模板方法 描述
(1) void acquire(int arg) 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法.
(2) void acquireInterruptibly(int arg) 与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回
(3) boolean tryAcquireNanos(int arg,long nanos)超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true
(4) void acquireShared(int arg) 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态
(5) void acquireSharedInterruptibly(int arg) 与acquireShared(int arg)相同,该方法相应中断
(6) boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
在acquireSharedInterruptibly(int arg)的基础上,增加了超时限制
(7) boolean release(int arg) 独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
(8) Boolean releaseShared(int arg) 共享式释放同步状态
(9) Collection
2.3同步器提供的模板方法基本上分为三类
(1) 独占式获取与释放同步状态
(2) 共享式获取与释放同步状态和查询同步队列中的等待线程情况
(3) 自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义
3.内部依赖类工具
同步器依赖于内部的FIFO双向等待队列来完成同步状态(锁)的管理,该等待队列是CLH(常用于自旋锁)队列的变种,同步器中的等待队列可以简单的理解为“等待锁的线程队列”。当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
3.1.同步器内部节点node类及信息
(1) Node SHARED 表示节点使用共享模式等待
(2) Node EXCLUSIVE 表示节点使用独占模式等待
(3) int waitStatus:
1) CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,该节点不会参与同步状态的竞争,需要从同步队列中取消等待,节点进入该状态后将不会再变化;
2) SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行;
3) CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中;
4) PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地传播下去;
5) 初始值为0
(4) Node prev:前驱节点,当节点加入同步队列时被设置(尾部添加)
(5) Node next:后继节点
(6) Node nextWaiter:等待队列中的后继节点。如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点公用同一个字段
(7) Thread thread:获取同步状态的线程
同步器维护了队列的头和尾,node之间通过prev和next应用连接.
3.2 节点入列操作
线程获取锁失败后会将该线程封装成node节点放入同步队列中,首先通过快速插入如果失败则调用enq()方法将节点放入队列中.
(1) 创建一个新的node节点.
(2) 判断tail是否为空,如果非空则直接插入尾部.
(3) 调用enq(node)方法确保node节点插入队列中.
通用方法compareAndSetTail()基于CAS原理实现,从存储层之间更改tail引用.
3.3 节点出队操作
出队比较简单直接设置head指向next,并将远head指向的node的引用置为空.
4.获取锁与释放锁
获取锁的过程又分为独占式和共享式,原理基本相似.下面主要讲独占式获取/释放锁,所谓独占式也就是同一时刻只能同一个线程获取到锁.
4.1同步器获取锁acquire()方法主要通过三步完成:
(1) tryAcquire由其子类进行实现,主要是独占式尝试获取锁
(2) addWaiter添加独占式节点到同步队列尾部并调用acquireQueued方法.
(3) acquireQueued如果返回true则中断线程.
当添加节点完成后立即尝试该节点是否能够成功acquire,这里主要讲一下acquireQueued(node,arg)方法主要代码流程如下:
(1) for(;;)进入自旋,node.predecessor()获取前置节点.如果前置节点为head则尝试获取锁,成功后则将该节点设置为head.返回false表示当前线程节点没有中断.
(2) shouldParkAfterFailedAcquire(p,node)校验node是否需要park(park:会将node的线程阻塞)只有当前驱节点等待状态waitStatus为SIGNAL,才能将node进行park,因为当前驱节点为SIGNAL时,会保证来唤醒自己,因此可以安心park.
(3) parkAndCheckInterrupt()node进入park(阻塞)状态,直到被前驱节点唤醒,被唤醒后返回线程是否为中断状态.
至此如果一切正常则没有获取锁的节点线程一直阻塞,head节点线程运行.
(4) cancleAcquire()取消正在进行的acquire尝试,走到这边代表出现异常.
cancleAcquire过程如下:
1) 首先判断该节点收否为空,如果空直接返回.
2) 如果node的前驱节点pred的waitStatus为CANCELLED,则向前寻找等待状态不为CANCELLED的前驱节点pred。
3) 将predNext赋值为pred节点的后继节点。
4) 将node的waitStatus设置为CANCELLED。
5) 如果node为尾节点,则使用CAS将尾节点改为pred节点(即将pred后面的节点全部移除,包括node节点和node前面状态为CANCELLED的节点),如果修改尾节点成功,则使用CAS将pred节点的后继节点设置为null。
6) 否则,node不是尾节点。判断pred节点是否可以提供给node后继节点唤醒信号。(pred可以提供唤醒信号:即pred满足以下条件:pred不为头节点 && (pred的等待状态为SIGNAL 或 pred的等待状态使用CAS成功修改为SIGNAL) && pred节点的线程不为null)如果可以,则将pred.next设置为node的后继节点,这样在pred释放的时候就会提供一个信号给node的后继节点,从而保证node的后继节点不受影响;如果pred节点无法提供给node的后继节点唤醒信号,则直接调用unparkSuccessor方法(详解见下文代码块7)唤醒node的后继节点。
7) unparkSuccessor(node)获取当前node的waitStatus
|---如果ws<0(可能为0/-1)即node的后续节点可能需要被signal,则使用CAS将status置为0.
|---遍历node到第一个status<0的nextNormalnode,并将中间的status=1的node删除然后直接唤醒nextNormalNode.
4.2同步器释放锁release(int arg)
(1) 首先是调用tryRelease方法,跟上文的tryAcquire一样,在AQS中该方法是没有实现的,子类必须实现。一般是用于解锁,例如再ReentrantLock中:tryRelease方法会将同步状态值(state)减去入参中要释放的值,如果减完的同步状态值为0,则将独占模式同步器的当前所有者设为null,即代表了解锁的意思。
(2) 如果tryRelease成功,并且head节点不为空,且状态不为初始状态,则调用unparkSuccessor方法唤醒head节点的后继节点。
5.ReentrantLock锁流程
ReentrantLock是唯一实现Lock的类,包含三个子类(1)Sync封装了公共的方法 (2)NoneFairSync继承Sync,重写了lock()和tryAcquire()方法.3.FairSync类重写了lock()和tryAcquire()方法.以NoneFairSync的lock方法作为入口探讨锁流程:
(1).NoneFairSync.lock()获取锁
1) 首先通过CAS方法设置锁状态,当state=0时表示可以获取获取成功后将state=1.继续执行当前线程...
2) 否则调用acquire(1)竞争锁,这里的参数1表示如果锁状态state为0将state=1.如果当前锁再次获取到锁则将state=state+1
(2) 如果获取锁失败则进入到同步队列AQS类方法acquire(1),开始进入到上述AQS的过程.首先调用ReentrantLock重写的AQS->tryAcquire()方法,获取锁失败后则添加到同步队列中.
(3) acquireQueued将当前没有获取到锁的线程循环在for(;;)中自旋,此过程中如果线程发生中断则进行cancleAcquire处理.
(4) 如果满足park(阻塞)条件将当前线程阻塞,有AQS的release()方法确保将该线程唤醒.