简单介绍
AQS即AbstractQuenedSynchronizer,中文名称抽象队列同步器,定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,像ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等。
实现原理
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于CLH 锁实现的。 CLH 锁其实是对自旋锁的一种改进,是一个虚拟的双向队列,暂时获取不到锁的线程将被加入到该队列中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
也就是说AQS中维护了一个volicate int state(共享资源)和一个FIFO的线程等待队列。
这里volatile 能够保证多线程下的可见性,当state=1 则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,并且会挂起,等待其他获取锁的线程释放锁才能够被唤醒,并且state值可以大于一,代表锁是可重入的。此外,对state的操作都是通过CAS来保证其修改并发性。
以可重入的互斥锁ReentrantLock 为例,它的内部维护了一个state 变量,用来表示锁的占用状态。state 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 lock() 方法时,会尝试通过 tryAcquire() 方法独占该锁,并让 state 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的)。这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。
节点状态
节点状态waitStatus,需要保证可见性,用volicate实现,waitStatus标识Node节点的状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
- CANCELLED(1): 表示节点已取消调度,当timeout或被中断时,会触发变更为此状态,进入此状态的节点将不会再变化。
- SIGNAL(-1): 表示后继节点在等待当前节点的唤醒。后继节点入队时,会将前继节点的状态更新为SIGNAL。
- CODINTION(-2): 表示节点等待在Condition上,当其他线程调用了Condition的singal()方法后,Condition状态的节点将从等待队列转移到同步队列中。
- PROPAGATE(-3): 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新节点入队时的默认状态。
如何自定义实现AQS
同步器的方法是基于模板方法实现的,我们只需要继承AbstractQueuedSynchronizer
并且重写指定方法,然后再使用时将AQS组合在自定义同步组件的视线中,并调用其模板方法,这些模板方法就会自动调用使用者的重写方法。
自定义同步器时需要重写以下方法:
Java
//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)
//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()