队列同步器介绍
队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,一般作为同步器组件的静态内部类,在同步器中仅定义了与状态相关的方法,且状态既可以独占获取也可以共享获取,这样就可以实现不同的同步组件(ReetrantLock、CountDownLatch等)。同步组件利用同步器进行锁的实现,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等顶层操作。 同步器的设计是基于模板方法模式的,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
同步器提供了3个方法来修改和访问同步状态:
1、getState():获取当前同步状态
2、setState(int newState):设置当前同步状态
3、compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能保证操作的原子性。
队列同步器的实现
下面主要从实现角度分析同步器是如何完成线程同步的,主要包括:同步队列、独占式同步状态的获取和释放、共享式同步状态的获取和释放等核心数据结构与模板方法。
1、同步队列
同步器利用同步队列来完成同步状态的管理。它是一个FIFO的双向队列,当线程获取状态失败时,同步器会将当前线程和等待状态等信息包装成尾节点放入同步队列中,同时会阻塞当前线程。当同步状态释放时,会唤醒首节点,使其尝试获取同步状态。
在节点加入过程中,会涉及到并发的问题,所以这个加入过程要确保线程安全,因此同步器提供了一个基于CAS设置尾节点的方法:compareAndSetTail(Node expect, Node update)。
在设置首节点过程中,首节点是通过获取同步状态成功的线程设置的,由于只有一个线程能够获取到同步状态,所以设置首节点的方法并不需要使用CAS来保证。只需要将首节点设置原首节点的后续节点并断开原节点的next引用即可。
2、独占式同步状态的获取
同步器是通过acquire()方法来获取同步状态的,该方法是模板方法:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
1、调用自定义同步器实现的tryAcquire(arg)获取同步状态,该方法保证线程安全。
2、如果获取同步状态失败,则构造同步节点(独占式),通过addWaiter方法放入同步队列的尾部
final boolean acquireQueued(final Node node, int arg) { try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } catch (RuntimeException ex) { cancelAcquire(node); throw ex; } }
3、节点在加入同步队列后,在同步队列中所有的节点都处于自旋状态,但是只有前驱节点是头节点才能尝试获取同步状态。一是因为头节点是已经获取到同步状态的节点,当头节点的线程释放同步状态之后,将会唤醒后继节点,后继节点被换唤醒后需要检查自己的前驱节点是否是头节点。而是因为这样处理可以维护同步队列的FIFO原则。
3、独占式同步状态的释放
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
该方法执行时,会唤醒节点的后继节点线程,通过调用unparkSuccessor(h)方法来唤醒处于等待状态的线程。
4、共享式同步状态获取
与独占式的区别在于:在同一时刻能否有多个线程同时获取到同步状态。例如文件读写,在同一时刻,如果进行读操作,那么写操作会被阻塞,但是可以同时有多个读操作,这种读操作就是共享式的。相反,写操作只能有一个,并且阻塞其他所有的写操作和读操作。
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } catch (RuntimeException ex) { cancelAcquire(node); throw ex; } }
1、调用tryAcquireShared(arg)尝试获取同步状态,如果方法值大于等于0,则表示获取同步状态成功。
2、在方法doAcquireShared自旋过程中,如果前驱节点为头节点时,尝试获取同步状态。自旋结束的条件就是tryAcquireShared方法的返回值大于等于0.
5、共享式同步状态释放
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
这个与独占式类似,都是释放后将会唤醒处于等待状态的节点。唯一的区别是这个方法必须支持并发,因为释放同步状态的操作会来自多个线程。所以tryReleaseShared(arg)是通过CAS保证的。