AbstractQueuedSynchronizer
AbstractQueuedSynchronizer用来构建锁或者其他同步组件的基础框架,使用一个volatile修饰的int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,就需要同步器提供的3个方法getState()、setState(int newState)和compareAndSetState(int expect, int update)来进行操作,因为它们能够保证状态的改变是安全的。
同步器基于模板设计模式实现的,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义的同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法会调用使用者重写的方法。
重写同步器指定方法时需要使用同步器提供的如下三个方法来访问或修改同步状态:
getState():获取当前同步状态 setState(int new State):设置当前同步状态 compareAndState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
waitStatus记录当前线程等待状态,可以为CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点)
同步器可重写的方法
   
实现自定义同步组件时,将会调用AbstractQueuedSynchronizer自身已经写好的模板方法,这些模板方法与描述:
   
注:模板方法基本分为三类:独占式同步状态获取与释放、共享式同步状态获取与释放和查询同步队列中等待线程情况。
/** * 头结点,你直接把它当做 当前持有锁的线程 */ private transient volatile Node head; /** * 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表 */ private transient volatile Node tail; /** * 代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁 * 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1 */ private volatile int state;
AbstractQueuedSynchronizer 的等待队列示意如下。阻塞队列不包含 head,不包含 head,不包含 head。
      
  1、同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,加入队列的过程必须保证线程安全,为什么必须保证线程安全?因为要面对同时有多条线程没有获取到同步状态要加入同步队列尾部的情况;
  2、队列首结点是获取同步状态成功的线程节点;
  3、前驱结点线程释放锁后将尝试唤醒后继结点中处于阻塞状态的线程
同步器将节点加入同步队列的过程:
  
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为新的首节点。
  
注:设置首节点是通过由已经获取到了同步状态的线程来完成的,由于只有一个线程能够获取到同步状态,因此设置头节点的方法并不需要CAS来保障,它只需要让head指针指向原首节点的后继节点并断开原首节点的next引用即可。
同步器依赖于内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构成一个节点(Node)并将其加入同步队列,同时阻塞当前线程,当同步状态释放时,会将首节点中的线程唤醒,使其再次尝试获取同步状态。
static final class Node { /** Marker to indicate a node is waiting in shared mode */ static final Node SHARED = new Node(); /** Marker to indicate a node is waiting in exclusive mode */ static final Node EXCLUSIVE = null; /** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; volatile Node next; /** * The thread that enqueued this node. Initialized on * construction and nulled out after use. */ volatile Thread thread; //等待队列中的后继节点,如果当前节点是共享的,那么这个字段将是SHARED常量,即节点类型(独占和共享)和等待队列中的后继节点共用同一个字段 Node nextWaiter; /** * Returns true if node is waiting in shared mode. */ final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
队列同步器提供的独占式同步状态获取方法
通过AbstractQueuedSynchronizer类提供的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是说由于线程获取同步状态失败后进入同步队列中,后继对线程进行中断操作时,线程不会从同步队列移除。acquire方法
public final void acquire(int arg) { //tryAcquie()方法具体要交给子类去实现,AbstractQueuedSynchronizer类中不实现 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
  首先调用自定义同步器(AbstractQueuedSynchronizer的子类)实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node),并调用LockSupport.park(this)方法挂起自己。
private Node addWaiter(Node mode) { 首先创建一个新节点,并将当前线程实例封装在内部,mode这里为null Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } ///入队的逻辑 enq(node); return node; } /** * 队列不空时向尾部添加结点的逻辑在enq(node)方法中也有,之所以会有这部分“重复代码”是对某些特殊情况进行提前处理,牺牲一定的代码可读性换取性能提升。 */ private Node enq(final Node node) { for (;;) { //t指向当前队列的最后一个节点,队列为空则为null Node t = tail; //队列为空 if (t == null) { //此时链表没有节点,需要初始化让head跟tail都指向一个哨兵节点 //构造新结点,CAS方式设置为队列首元素,当head==null时更新成功 if (compareAndSetHead(new Node())) tail = head;//尾指针指向首结点 } else { //队列不为空 node.prev = t; if (compareAndSetTail(t, node)) { //CAS将尾指针指向当前结点,当t(原来的尾指针)==tail(当前真实的尾指针)时执行成功 t.next = node; //原尾结点的next指针指向当前结点 return t; } } } } final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //死循环,正常情况下线程只有获得锁才能跳出循环 for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { //将当前结点设置为队列头结点 setHead(node); p.next = null; // help GC failed = false; //正常情况下死循环唯一的出口 return interrupted; } //判断是否要阻塞当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
enq(final Node node)方法后,发现事实上AQS队列的头节点其实是个哨兵节点。
在enq(final Node node)中,同步器通过死循环的方式来确保节点的添加,在死循环中只有通过CAS将当前节点设置为尾节点之后,当前线程才能从该方法返回,否则的话当前线程不断地尝试设置。enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。循环加CAS 操作是实现乐观锁的标准方式,CAS是为了实现原子操作而出现的,所谓的原子操作指操作执行期间,不会受其他线程的干扰。 Java实现的 CAS是调用unsafe类提供的方法,底层是调用C++方法,直接操作内存,在CPU层面加锁,直接对内存进行操作
shouldParkAfterFailedAcquire(Node pred, Node node),从方法名上我们可以大概猜出这是判断是否要阻塞当前线程的,源码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //状态为SIGNAL /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { //状态为CANCELLED, /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //状态为初始化状态(ReentrentLock语境下) /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
针对前驱结点pred的状态会进行不同的处理:
  pred状态为SIGNAL,则返回true,表示要阻塞当前线程;
  pred状态为CANCELLED,则一直往队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面;
  pred的状态为初始化状态,此时通过CAS操作将pred的状态改为SIGNAL
其实这个方法的含义很简单,就是确保当前结点的前驱结点的状态为SIGNAL,SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程。毕竟,只有确保能够被唤醒,当前线程才能放心的阻塞。
  要注意只有在前驱结点已经是SIGNAL状态后才会执行后面的方法立即阻塞,对应上面的第一种情况。其他两种情况则因为返回false而重新执行一遍
  acquireQueued()方法的源码表明节点在进入队列后,就进入了一个自旋状态,每个节点(或者说每个线程),都在自省观察,当条件满足,获取到同步状态,就可以从这个自旋过程中退出,否则依旧留在自旋过程中
  
在acquireQueued(final Node node, int arg)方法中,线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点时才能够尝试获取同步状态,原因如下:
  头节点是成功获取到同步状态的节点,而头节点线程获取到同步状态后,将会唤醒其后继节点,后继节点的线程在被唤醒后需要检查自己的前驱节点是否是头节点;
  维护同步队列的FIFO原则:可以看到节点与及节点之间在循环检查的过程中基本上不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放符合FIFO,并且对于方便对过早通知进行处理。
独占式同步状态获取流程如下图,也就是acquire(int arg)方法的执行流程:
  
队列同步器提供的独占式同步状态释放方法
释放同步状态,通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)
public final boolean release(int arg) { //tryRelease()方法具体要交给子类去实现,AbstractQueuedSynchronizer类中不实现 if (tryRelease(arg)) { Node h = head; //当前队列不为空且头结点状态不为初始化状态(0) if (h != null && h.waitStatus != 0) unparkSuccessor(h); //唤醒同步队列中被阻塞的线程 return true; } return false; }
若当前线程已经完全释放锁,即锁可被其他线程使用,则还应该唤醒后续等待线程。需要进行两个条件的判断:
  1、h != null是为了防止队列为空,即没有任何线程处于等待队列中,那么也就不需要进行唤醒的操作;
  2、h.waitStatus != 0是为了防止队列中虽有线程,但该线程还未阻塞,由前面的分析知,线程在阻塞自己前必须设置其前驱结点的状态为SIGNAL,否则它不会阻塞自己;
  会唤醒头节点的后继节点线程,unparkSuccerssor(Node node)方法使用LcokSupport来唤醒处于等待(阻塞)状态的线程
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; //从尾部向头部遍历 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
一般情况下只要唤醒后继结点的线程就行了,但是后继结点可能已经取消等待,所以从队列尾部往前回溯,找到离头结点最近的正常结点,并唤醒其线程。
独占式同步状态获取和释放:
  在获取同步状态时,同步器会维持一个同步队列,获取失败的线程都会被加入到同步队列中,并在同步队列中自旋(判断自己前驱节点为头节点)。
  移出队列(停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
共享式同步状态获取与释放
共享式获取与独占式获取的区别: 同一时刻能否有多个线程同时获取到同步状态。
以文件的读写为例:
  如果有一个程序在读文件,那么这一时刻的写操作均被阻塞,而读操作能够同时进行。
  如果有一个程序在写文件,那么这一时刻不管是其他的写操作还是读操作,均被阻塞。
  写操作要求对资源的独占式访问,而读操作可以是共享式访问。
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } private void doAcquireShared(int arg) { //当前节点加入同步队列 final Node node = addWaiter(Node.SHARED); boolean failed = true; 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(); failed = false; return; } } //用LockSupport的park方法把当前线程阻塞住 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
当前线程首先调用tryAcquireShared()这个被子类重写的方法,共享式的获取同步状态。如果返回值大于等于0,表示获取成功并返回。
  如果返回值小于0表示获取失败,调用doAcquireShared()方法,让线程进入自旋状态。
  自旋过程中,如果当前节点的前驱节点是头结点,且调用tryAcquireShared()方法返回值大于等于0,则退出自旋。否则,继续进行自旋。
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
首先去尝试释放资源tryReleaseShared(arg),如果释放成功了,就代表有资源空闲出来,那么就用doReleaseShared()去唤醒后续结点。
参考:
      https://blog.csdn.net/fuzhongmin05/article/details/104721667
https://www.jianshu.com/p/e7659436538b
https://juejin.cn/post/6844903872939425805
https://www.cnblogs.com/leesf456/p/5350186.html
https://www.cnblogs.com/sanzao/p/10657020.html
  https://www.corgiboy.com/Java%E5%9F%BA%E7%A1%80/Java%E5%9F%BA%E7%A1%80-AQS%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
  
                    
                
                
            
        
浙公网安备 33010602011771号