AQS原理简介
AQS简介
-
原名 AbstractQueuedSynchronizer 即队列同步器 是构建锁和其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch)
-
AQS解决了子类实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列 自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了,所以使用AQS不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题
-
只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量
-
AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态
深入源码 看看有哪些方法
- getState()
返回同步状态的当前值
- setState(int newState)
设置当前同步状态
- compareAndSetState(int expect, int update)
使用CAS设置当前状态,该方法能够保证状态设置的原子性
自定义同步器主要实现以下几个方法
- tryAcquire(int arg)
独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态
- tryRelease(int arg)
独占式释放同步状态
- tryAcquireShared(int arg)
共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败
- tryReleaseShared(int arg)
共享式释放同步状态
- tryReleaseShared(int arg)
当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占
- acquire(int arg)
独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法
- acquireInterruptibly(int arg)
与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回
- tryAcquireNanos(int arg,long nanos)
超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true
- acquireShared(int arg)
共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态
- acquireSharedInterruptibly(int arg)
共享式获取同步状态,响应中断
- tryAcquireSharedNanos(int arg, long nanosTimeout)
共享式获取同步状态,增加超时限制
- release(int arg)
独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
- releaseShared(int arg)
共享式释放同步状态
CLH
CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,
当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,
当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
在CLH同步队列中,一个节点表示一个线程,它保存着
线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),其数据结构如下
同步队列的数据结构
入列
CHL这种链表式结构入列,无非就是tail指向新节点、新节点的前驱节点指向当前最后的节点,当前最后一个节点的next指向当前节点
- addWaiter(Node node)
将当前线程加入到等待队列的队尾,并返回当前线程所在的结点
addWaiter(Node node)先通过快速尝试设置尾节点,如果失败,则调用enq(Node node)方法设置尾节点
此方法用于将node加入队尾,该方法核心就是通过CAS自旋的方式来设置尾节点,知道获得预期的结果即添加节点成功,当前线程才会返回
出列
CLH同步队列遵循FIFO(先进先出),首节点的线程释放同步状态后,
将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单,
head执行该节点并断开原首节点的next和当前节点的prev即可,
注意在这个过程是不需要使用CAS来保证的,因为只有一个线程能够成功获取到同步状态。
同步状态的获取与释放
独占式同步状态获取
如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响
-
tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法由自定义同步组件自己实现(通过state的get/set/CAS),该方法必须要保证线程安全的获取同步状态。
-
addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部,并标记为独占模式。
-
acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;如果在整个等待过程中被中断过,则返回true,否则返回false。
-
selfInterrupt:如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
当前线程会一直尝试获取同步状态,当然前提是只有其前驱节点为头结点才能够尝试获取同步状态
1、保持FIFO同步队列原则。
2、头节点释放同步状态后,将会唤醒其后继节点,后继节点被唤醒后需要检查自己是否为头节点。
这段代码主要检查当前线程是否需要被阻塞,具体规则如下:
[1]如果当前线程的前驱节点状态为SINNAL,则表明当前线程需要被阻塞,
调用unpark()方法唤醒,直接返回true,当前线程阻塞
[2]如果当前线程的前驱节点状态为CANCELLED(ws > 0),
则表明该线程的前驱节点已经等待超时或者被中断了,
则需要从CLH队列中将该前驱节点删除掉,直到回溯到前驱节点状态 <= 0 ,返回false
[3]如果前驱节点非SINNAL,非CANCELLED,
则通过CAS的方式将其前驱节点设置为SINNAL,返回false
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能被阻塞,
需要去找个安心的休息点(前驱节点状态 <= 0 ),同时可以再尝试下看有没有机会去获取资源。
如果 shouldParkAfterFailedAcquire(Node pred, Node node) 方法返回true,则调用parkAndCheckInterrupt()方法阻塞当前线程