一、AQS是什么
AQS是AbstractQueuedSynchronizer的简称,它是Java各种锁的底层实现,内部有一个int类型的volatile修饰变量表示同步状态,并提供一系列的CAS操作来管理这个同步状态。
AQS是JAVA中各种锁机制的底层实现,如同步工具类Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock、FutureTask,CyclicBarrier都是是基于AQS实现的,你也可以基于AQS去实现自己的同步工具类,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
总的来说AQS是一个同步器,采用模板方法设计模式,核心数据结构:双向链表 + state(锁状态),通过CAS底层操作来维护同步状态提供锁机制的底层实现。
为了更好理解AbstractQueuedSynchronizer的运行机制,可以首先研究其内部数据结构,这里整理成如下图:
AQS内维护一个volatile修饰的int型的变量 state用于记录锁的状态,该值为共享资源>1表示被锁定,state==0表示未被锁定
一个继承自AbstractOwnableSynchronizer类的Thread类型变量exclusiveOwnerThread用于指向当前获取排他锁的线程
两个AbstractQueuedSynchronizer.Node类型的变量head及tail,用来标识队列的头和尾。
这几个字段都用 volatile 关键字进行修饰,以确保多线程间保证字段的可见性。
再细看Node类型为AbstractQueuedSynchronizer的内部类,这个内部类里面对线程进行了封装,并定义了很多属性
1、Node类型
a、mode用来表示队列中个线程要获取锁的类型:
分为 SHARED(共享)、EXCLUSIVE(排它锁)
b、prev:volatile 修饰的变量,标识当前节点的前驱节点,当前线程依赖它来检查waitStatus,在入队的时候分配
c、next:volatile 修饰的变量,标识当前节点的后继节点,当前线程释放的时候才被唤醒,在入队时分配
2、Thread类型:
thread:当前节点的线程,在初始化时使用,使用后失效
3、int类型
waitStatus:标识当前线程等待的状态,对应的值和解释如下
static final int CANCELLED =1;
取消状态,如果当前线程的前置节点状态为 CANCELLED,则表明前置节点已经等待超时或者已经被中断了,这时需要将其从等待队列中删除。
static final int SIGNAL = -1;
等待触发状态,后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程继续运行,如果当前线程的前置节点状态为 SIGNAL,则表明当前线程需要阻塞
static final int CONDITION = -2;
等待条件状态,表示当前节点在等待 condition,即在 condition 队列中,当其他线程对Condition调用了 signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
static final int PROPAGATE = -3;
状态需要向后传播,表示下一次共享式同步状态获取将会无条件地被传播下去(可能一个一个节点传播下去),仅在共享锁模式下使用。
AQS是通过维护一个双向链表队列(CLH)实现的,通过锁自旋不断轮询前驱节点的状态,发现前驱释放了锁就结束自旋,
我们来看下它的流程图
同步队列中首节点是获取到锁的节点,它在释放的时候会唤醒后继节点,后继节点获取到锁的时候,会把自己设为新的首节点。
源码分析:AQS 提供了两种锁,分别是独占锁和共享锁
独占锁指的是操作被认作一种独占操作,比如 ReentrantLock,它实现了独占锁的方法
共享锁则指的是一个非独占操作,比如一些同步工具 CountDownLatch 和 Semaphore 等同步工具,下面是 AQS 对这排它锁提供的抽象方法。
排它锁:以ReentrantLock为例
核心方法 aquaire和release及他们方法体里使用到的方法。
通过 tryAcquire(arg) 方法尝试获取锁,这个方法需要实现类自己实现获取锁的逻辑,获取锁成功后则不执行后面加入等待队列的逻辑了;
如果尝试获取锁失败后,则执行 addWaiter(Node.EXCLUSIVE) 方法将当前线程封装成一个 Node 节点对象,并加入队列尾部;
把当前线程执行封装成 Node 节点后,继续执行 acquireQueued 的逻辑,该逻辑主要是判断当前节点的前置节点是否是头节点,来尝试获取锁,如果获取锁成功,则当前节点就会成为新的头节点。
这个方法首先获取当前线程,再获取当前节点的同步状态c
如果是c=0:0为节点入队时的初始状态,则需进一步调用hasQueuedPredecessors()方法判断这个节点之前是否还有等待获取锁的节点(当前节点是不是需要排队 )。
如果hasQueuedPredecessors返回true,则表示获取锁失败直接返回false
如果hasQueuedPredecessors返回false,则通过CAS操作设置同步状态,再将当前线程设置为获取排他锁的线程,最后返回true表示当前线程获取锁成功。
如果c!=0 ,则进一步判断currentThread==exclusiveOwnerThread,ture则将state同步状态+1,然后设置state返回获取锁成功,false则返回获取锁失败。
注意图中红框出为什么不需要用CAS?因为这里是排它锁,而且当前线程已经获取到锁,所以直接调用setState是线程安全的
看下hasQueuedPredecessors是怎么判断当前节点是怎么判断是否存在等待获取锁的前驱节点的,主要判断当前线程需不需要排队
如果head节点!=tail节点,表示至少有两个节点,进一步如果head节点的下一个节点的线程不是当前线程则表示同步列表中至少存在3个节点,当前线程前面还有线程在等待获取锁。
再来看看当tryAcquire获取锁失败时,调用的addWaiter方法做了些什么
1、新建一个节点封装当前线程
2、判断尾节点是否为空,不为空则将tail节点设置为新节点的前驱节点,通过CAS操作设置tail节点,将原来的tail节点的next指向新的node
3、如果tail节点为空则调用enq方法:enq方法是通过自旋的方式直到新节点设置成功
跟进enq方法
无限循环直至t!=null且成功设置tail节点,下面具体解析下这个方法
如果tail为空则通过CAS的方式设置head,也就是初始化一个同步队列,并将head赋值给tail
下轮循环tail不再为空,则将tail这是为新节点node的前驱节点,在通过CAS操作设置新的tail
最后将原来tail的next指向node并返回。
根据源码分析获取锁失败之后,调用addWaiter创建新节点加到同步队列尾部,在调用acquireQueued方法,这个方法做什么用呢?继续跟进
通过代码可以发现这个方法通过自旋的方式进行处理
1、调用predecessor方法获取当前节点的前驱节点p
2、p==head再次调用tryAcquire尝试获取锁,如果获取成功则将当前节点设置为head,调用p.next=null释放原来的head,当前线程不中断
3、如果p!=head或者tryAcquire获取锁失败,则进行挂起逻辑
注意:head 节点代表当前持有锁的线程,如果当前节点的 pred 节点是 head 节点,很可能此时 head 节点已经释放锁了,所以此时需要再次尝试获取锁
再来看下判断是否需要挂起逻辑的源码
1、获取ored的等待状态 ws
2、ws==SIGNAL:当前节点需要挂起
3、ws>0:表示pred节点已经被取消,需要在同步队列中剔除,剔除所有取消的节点
4、ws==0||ws==-3:通过CAS将pred的等待状态设置为SIGMAL,再从 acquireQueued 方法自旋操作从新循环一次判断。
继续看挂起逻辑
首先调用LockSupport.park方法阻塞线程,在调用Thread.interrupted方法返回阻塞是否阻塞成功
释放锁:
首先尝试释放锁,如果返回true表示能够释放锁,进一步判断如果head不为空且等待状态!=0则通过unparkSuccessor唤醒后继节点,看下怎么尝试释放锁的
1、获取当前节点的state值-1得到c
2、如果当前线程不是获取排他锁的线程则抛异常
3、如果c==0 标记为可释放,并把当前节点的exclusiveOwnerThread设置为空
4、c!=0,这只state并返回是否可释放的标志
在看下unparkSuccessor的源码
释放锁主要是将头节点的后继节点唤醒,如果后继节点不符合唤醒条件,则从队尾一直往前找,直到找到符合条件的节点为止。
共享锁:以Semaphore为例进行分析
先看下获取共享锁的源码
从代码可以看出也是通过自旋的方式获取锁
首先看当前线程是否需要排队,如果需要排队则返回-1;
不需要排队则获取当前节点的state值
判断余量<0? CAS设置同步状态并返回余量值remaining,返回值小于0 获取共享锁失败,调用doAcquireShared(arg)方法,接着往下看
1、增加一个SHARED类型的节点,并添加到同步队列的尾部
2、获取node的pred节点p
3、p==head,再次尝试获取共享锁:成功则调用setHeadAndPropagate方法设置头并传播唤醒的动作.
4、将p.next=null,释放
5、如果p不是头节点,则执行阻塞逻辑和排它锁逻辑一样,不再赘述
跟进setHeadAndPropagate方法
1、将老的head复制给h
2、设置新的头节点
3、如果propagate>0 标识需要传播唤醒;h==null||h.waitStatus<0;新的头为null||新的头的waitStatus<0 ; 获取node的后继节点s,s==null||s的获取锁的模式是SHARED则调用doReleaseShared方法下面是该方法的源码
依然是通过自旋来处理的,当h==head时跳出循环
head不为空&&head!=tail时获取head的waitStatus值ws
1、ws==SIGNAL 则将节点的SIGNAL设置为0,如果设置失败则进入下轮循环
2、设置成功则调用unparkSuccessor唤醒后继节点
3、ws==0,则将节点的等待状态设置为PROPAGATE,表示可以向后继续传播,如果设置失败一直循环直到成功
释放共享锁
首先尝试释放共享锁,返回true则调用doReleaseShared()方法上面已经解析过了我们看下tryReleaseShared方法的代码
1、获取当前节点的同步状态 state
2、重置信号量,设置成功则返回true,否则一直循环直到成功