AQS
AQS(AbstractQueuedSynchronizer)是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量来表示状态,通过内置的FIFO(first in,first out)队列来完成资源获取线程的排队工作。
同步队列
同步队列(一个FIFO双向队列)是AQS的核心,用来完成同步状态的管理,当线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构造成一个节点并加入到同步队列,同时会阻塞当前线程。
同步状态
AQS中维持一个全局的int状态码state,线程通过修改(加/减指定的数量)码是否成功来决定当前线程是否成功获取到同步状态。
AQS支持两种获取同步状态的模式既独占式和共享式。顾名思义,独占式模式同一时刻只允许一个线程获取同步状态,而共享模式则允许多个线程同时获取。
独占模式获取与释放状态
独占模式既同一时间只能由一个线程持有同步状态。当多个线程竞争时(acquire),获取到同步状态的线程会将当前线程赋值给Thread exclusiveOwnerThread
属性(AQS父类中)来标记当前状态被线程独占。其他线程将被构造成Node加入到同步队列中。
获取同步状态
1 | /** |
- 多线程并发获取(修改)同步状态, 修改同步状态成功的线程标记为拥有同步状态
- 获取失败的线程,加入到同步队列的队尾;加入到队列中后,如果当前节点的前驱节点为头节点再次尝试获取同步状态。如果头节点的下一个节点尝试获取同步状态失败后,会进入等待状态;其他节点则继续自旋。
总结:
释放同步状态
当线程执行完相应逻辑后,需要释放同步状态,使后继节点有机会同步状态(让出资源,让排队的线程使用)。这时就需要调用release(int arg)方法。调用该方法后,会唤醒后继节点。
- 释放同步状态,唤醒后继节点
后继节点获取同步状态成功,头节点出队。需要注意的事,出队操作是间接的,有节点获取到同步状态时,会将当前节点设置为head,而原本的head设置为null。
当同步队列中头节点唤醒后继节点时,此时可能有其他线程尝试获取同步状态。假设获取成功,将会被设置为头节点。头节点后续节点获取同步状态失败。
共享模式获取与释放状态
共享模式和独占模式最主要的区别是在支持同一时刻有多个线程同时获取同步状态。为了避免带来额外的负担,在上文中提到的同步队列中都是用独占模式进行讲述,其实同步队列中的节点应该是独占和共享节点并存的。
获取同步状态
- 首先至少要调用一次tryAcquireShared(arg)方法,如果返回值大于等于0表示获取成功。
- 当获取锁失败时,则创建一个共享类型的节点并进入一个同步队列,然后进入队列中进入自旋状态(阻塞,唤醒两种状态来回切换,直到获取到同步状态为止)
- 当队列中的等待线程被唤醒以后就重新尝试获取锁资源,如果成功则唤醒后面还在等待的共享节点并把该唤醒事件传递下去,即会依次唤醒在该节点后面的所有共享节点,否则继续挂起等待。
当一个同享节点获取到同步状态,并唤醒后面等待的共享状态的结果如下图所示:
释放同步状态
释放同步状态后,同步队列的变化过程和共享节点获取到同步状态后的变化过程一致,此处不再进行赘述。
总结
- AQS通过一个int同步状态码,和一个(先进先出)队列来控制多个线程访问资源
- 支持独占和共享两种模式获取同步状态码
- 当线程获取同步状态失败会被加入到同步队列中
- 当线程释放同步状态,会唤醒后继节点来获取同步状态
- 共享模式下的节点获取到同步状态或者释放同步状态时,不仅会唤醒后继节点,还会向后传播,唤醒所有同步节点
- 使用volatile关键字保证状态码在线程间的可见性,CAS操作保证修改状态码过程的原子性。
条件队列
条件队列:当某个线程调用了wait方法,或者通过Condition对象调用了await相关方法,线程就会进入阻塞状态,并加入到对应条件队列中。
即当对象获取到同步锁之后,如果调用了wait方法,当前线程会进入到条件队列中,并释放锁。
1 | synchronized(对象){ // 获取锁失败,线程会加入到同步队列中 |
基于synchcronized的内置条件队列存在一些缺陷。每个内置锁都只能有一个相关联的条件队列,因而存在多个线程可能在同一个条件队列上等待不同的条件谓词,并且在最常见的加锁模式下公开条件队列对象。
与Object配合synchronized相比,基于AQS的Lock&Condition实现的等待唤醒模式更加灵活,支持多个条件队列,支持等待状态中不响应中断以及超时等待功能; 其次就是基于AQS实现的条件队列是”肉眼可见”的,我们可以通过源代码进行debug,而synchronized则是完全隐式的。
同步队列和条件队列
与条件队列密不可分的类则是ConditionObject, 是AQS中实现了Condition接口的内部类,通常配合基于AQS实现的锁一同使用。当线程获取到锁之后,可以调用await方法进入条件队列并释放锁,或者调用singinal方法唤醒对应条件队列中等待时间最久的线程并加入到等待队列中。
在AQS中,线程会被封装成Node对象加入队列中,而条件队列中则复用了同步队列中的Node对象。
Condition相关方法和使用
Condition接口一共定义了以下几个方法:
- await(): 当前线程进入等待状态,直到被通知(siginal)或中断【和wait方法语义相同】。
- awaitUninterruptibly(): 当前线程进入等待状态,直到被通知,对中断不敏感。
- awaitNanos(long timeout): 当前线程进入等待状态直到被通知(siginal),中断或超时。
- awaitUnitil(Date deadTime): 当前线程进入等待状态直到被通知(siginal),中断或到达某个时间。
- signal(): 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition关联的锁【和notify方法语义相同】
- signalAll(): 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition关联的锁【和notifyAll方法语义相同】。
使用
下面是一个线程等待与唤醒的例子,其中用1234序号标出了日志输出顺序。
1 | public static void main(String[] args) { |
分析:
- 线程1调用了reentrantLock.lock(),线程进入AQS等待队列,输出”wait signal”
- 接着调用了awiat方法,线程从AQS队列中移除,锁释放,直接加入condition的等待队列中
- 线程2因为线程1释放了锁,拿到了锁,输出”get lock”
- 线程2执行condition.signalAll()发送信号,输出”send signal”
- condition队列中线程1的节点接收到信号,从condition队列中拿出来放入到了AQS的等待队列,这时线程1并没有被唤醒。
- 线程2调用unlock释放锁,因为AQS队列中只有线程1,因此AQS释放锁按照从头到尾的顺序,唤醒线程1
- 线程1继续执行,输出”get signal”,并进行unlock操作。
条件队列入队操作
当线程获取到锁之后,Condition对象调用await相关的方法,线程会从同步队列中退出,进入到对应的条件队列中。
条件队出队操作
Condition对象调用signal或者signalAll方法时,方法唤醒对应条件队列中的相关线程并加入到同步队列中。
总结
- 条件队列和同步队列在Java中有两种实现:synchronized关键字和基于AQS
- 每个(基于synchronized的)内置锁都只能有一个相关联的条件队列,会存在多个线程可能在同一个条件队列上等待不同的条件谓词;而(基于AQS实现的)显式锁支持多个条件队列
- 与wait,notify,notifyAll 对应的方法时Conditoin接口中的await,signal,signalAll,他们具有相同的语义