1.什么是AQS?
AQS的核心思想是基于volatile int state这样的volatile变量,配合Unsafe工具对其原子性的操作来实现对当前锁状态进行修改。同步器内部依赖一个FIFO的双向队列来完成资源获取线程的排队工作。
2.同步器的应用
同步器主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态的修改或者访问主要通过同步器提供的3个方法:
- getState() 获取当前的同步状态
- setState(int newState) 设置当前同步状态
- compareAndSetState(int expect,int update) 使用CAS设置当前状态,该方法能够保证状态设置的原子性。
同步器可以支持独占式的获取同步状态,也可以支持共享式的获取同步状态,这样可以方便实现不同类型的同步组件。
同步器也是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。
3.AQS同步队列
同步器AQS内部的实现是依赖同步队列(一个FIFO的双向队列,其实就是数据结构双向链表)来完成同步状态的管理。
当前线程获取同步状态失败时,同步器AQS会将当前线程和等待状态等信息构造成为一个节点(node)加入到同步队列,同时会阻塞当前线程;
当同步状态释放的时候,会把首节点中的线程唤醒,使首节点的线程再次尝试获取同步状态。AQS是独占锁和共享锁的实现的父类。
4.AQS锁的类别:分为独占锁和共享锁两种。
- 独占锁:锁在一个时间点只能被一个线程占有。根据锁的获取机制,又分为“公平锁”和“非公平锁”。等待队列中按照FIFO的原则获取锁,等待时间越长的线程越先获取到锁,这就是公平的获取锁,即公平锁。而非公平锁,线程获取的锁的时候,无视等待队列直接获取锁。ReentrantLock和ReentrantReadWriteLock.Writelock是独占锁。
- 共享锁:同一个时候能够被多个线程获取的锁,能被共享的锁。JUC包中ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch和Semaphore都是共享锁。
JUC包中的锁的包括:Lock接口,ReadWriteLock接口;Condition条件,LockSupport阻塞原语。
AbstractOwnableSynchronizer/AbstractQueuedSynchronizer/AbstractQueuedLongSynchronizer三个抽象类,
ReentrantLock独占锁,ReentrantReadWriteLock读写锁。CountDownLatch,CyclicBarrier和Semaphore也是通过AQS来实现的。
下面是AQS和使用AQS实现的一些锁,以及通过AQS实现的一些工具类的架构图:
图 1.依赖AQS实现的锁和工具类
5.AQS同步器的结构:同步器拥有首节点(head)和尾节点(tail)。同步队列的基本结构如下:
图 1.同步队列的基本结构 compareAndSetTail(Node expect,Node update)
- 同步队列设置尾节点(未获取到锁的线程加入同步队列): 同步器AQS中包含两个节点类型的引用:一个指向头结点的引用(head),一个指向尾节点的引用(tail),当一个线程成功的获取到锁(同步状态),其他线程无法获取到锁,而是被构造成节点(包含当前线程,等待状态)加入到同步队列中等待获取到锁的线程释放锁。这个加入队列的过程,必须要保证线程安全。否则如果多个线程的环境下,可能造成添加到队列等待的节点顺序错误,或者数量不对。因此同步器提供了CAS原子的设置尾节点的方法(保证一个未获取到同步状态的线程加入到同步队列后,下一个未获取的线程才能够加入)。 如下图,设置尾节点:
图 2.尾节点的设置 节点加入到同步队列
- 同步队列设置首节点(原头节点释放锁,唤醒后继节点):同步队列遵循FIFO,头节点是获取锁(同步状态)成功的节点,头节点在释放同步状态的时候,会唤醒后继节点,而后继节点将会在获取锁(同步状态)成功时候将自己设置为头节点。设置头节点是由获取锁(同步状态)成功的线程来完成的,由于只有一个线程能够获取同步状态,则设置头节点的方法不需要CAS保证,只需要将头节点设置成为原首节点的后继节点 ,并断开原头结点的next引用。如下图,设置首节点:
图 3.首节点的设置
6.独占式的锁的获取:调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,即线程获取同步状态失败后进入同步队列,后续对线程进行中断操作时,线程不会从同步队列中移除。
(1) 当前线程实现通过tryAcquire()方法尝试获取锁,获取成功的话直接返回,如果尝试失败的话,进入等待队列排队等待,可以保证线程安全(CAS)的获取同步状态。
(2) 如果尝试获取锁失败的话,构造同步节点(独占式的Node.EXCLUSIVE),通过addWaiter(Node node,int args)方法,将节点加入到同步队列的队列尾部。
(3) 最后调用acquireQueued(final Node node, int args)方法,使该节点以死循环的方式获取同步状态,如果获取不到,则阻塞节点中的线程。acquireQueued方法当前线程在死循环中获取同步状态,而只有前驱节点是头节点的时候才能尝试获取锁(同步状态)( p == head && tryAcquire(arg))。
原因是:1.头结点是成功获取同步状态的节点,而头结点的线程释放锁以后,将唤醒后继节点,后继节点线程被唤醒后要检查自己的前驱节点是否为头结点。
2.维护同步队列的FIFO原则,节点进入同步队列以后,就进入了一个自旋的过程,每个节点(后者说每个线程)都在自省的观察。
下图为节点自旋检查自己的前驱节点是否为头结点:
图 4 节点自旋获取同步状态
独占式的锁的获取源码: acquire方法源码如下
1 /** 2 * Acquires in exclusive(互斥) mode, ignoring(忽视) interrupts. Implemented 3 * by invoking at least once {@link #tryAcquire}, 4 * returning on success. Otherwise the thread is queued(排队), possibly 5 * repeatedly(反复的) blocking and unblocking, invoking {@link 6 * #tryAcquire} until success. This method can be used 7 * to implement method {@link Lock#lock}. 8 * 9 * @param arg the acquire argument. This value is conveyed(传达) to 10 * {@link #tryAcquire} but is otherwise uninterpreted and 11 * can represent anything you like. 12 * 13 * 独占式的获取同步状态 14 * 15 */ 16 public final void acquire(int arg) { 17 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 18 selfInterrupt(); 19 }
尝试获取锁:tryAcquire方法:如果获取到了锁,tryAcquire返回true,反之,返回false。
1 //方法2: 2 protected final boolean tryAcquire(int acquires) { 3 // 获取当前线程 4 final Thread current = Thread.currentThread(); 5 // 获取“独占锁”的状态,获取父类AQS的标志位 6 int c = getState(); 7 //c == 0 意思是锁(同步状态)没有被任何线程所获取 8 //1.当前线程是否是同步队列中头结点Node,如果是的话,则获取该锁,设置锁的状态,并设置锁的拥有者为当前线程 9 if (c == 0) { 10 if (!hasQueuedPredecessors() && 11 12 // 修改下状态为,这里的acquires的值是1,是写死的调用子类的lock的方法的时候传进来的,如果c == 0,compareAndSetState操作会更新成功为1. 13 compareAndSetState(0, acquires)) { 14 // 上面CAS操作更新成功为1,表示当前线程获取到了锁,因为将当前线程设置为AQS的一个变量中,代表这个线程拿走了锁。 15 setExclusiveOwnerThread(current); 16 return true; 17 } 18 } 19 //2.如果c不为0,即状态不为0,表示锁已经被拿走。 20 //因为ReetrantLock是可重入锁,是可以重复lock和unlock的,所以这里还要判断一次,获取锁的线程是否为当前请求锁的线程。 21 else if (current == getExclusiveOwnerThread()) { 22 //如果是,state继续加1,这里nextc的结果就会 > 1,这个判断表示获取到的锁的线程,还可以再获取锁,这里就是说的可重入的意思 23 int nextc = c + acquires; 24 if (nextc < 0) 25 throw new Error("Maximum lock count exceeded"); 26 setState(nextc); 27 return true; 28 } 29 return false; 30 }
addWaiter方法的源码:回到aquire方法,如果尝试获取同步状态(锁)失败的话,则构造同步节点(独占式的Node.EXCLUSIVE), 通过addWaiter(Node node,int args)方法 将该节点加入到同步队列的队尾。
1 /** 2 * Creates and enqueues node for current thread and given mode. 3 * 4 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared 5 * @return the new node 6 * 7 * 8 * 如果尝试获取同步状态失败的话,则构造同步节点(独占式的Node.EXCLUSIVE),通过addWaiter(Node node,int args)方法将该节点加入到同步队列的队尾。 9 * 10 */ 11 private Node addWaiter(Node mode) { 12 // 用当前线程够着一个Node对象,mode是一个表示Node类型的字段,或者说是这个节点是独占的还是共享的,或者说AQS的这个队列中,哪些节点是独占的,哪些节点是共享的。 13 Node node = new Node(Thread.currentThread(), mode); 14 // Try the fast path of enq; backup to full enq on failure 15 Node pred = tail; 16 //队列不为空的时候 17 if (pred != null) { 18 node.prev = pred; 19 // 确保节点能够被线程安全的添加,使用CAS方法 20 // 尝试修改为节点为最新的节点,如果修改失败,意味着有并发,这个时候进入enq中的死循环,进行“自旋”的方式修改 21 if (compareAndSetTail(pred, node)) { 22 pred.next = node; 23 return node; 24 } 25 } 26 //进入自旋 27 enq(node); 28 return node; 29 }
enq方法的源码:同步器通过死循环的方式来保证节点的正确添加,在“死循环” 中通过CAS将节点设置成为尾节点之后,当前线程才能从该方法中返回,否则 当前线程不断的尝试设置。 enq方法将并发添加节点的请求通过CAS变得“串行化”了。
1 /** 2 * Inserts node into queue, initializing if necessary. See picture above. 3 * @param node the node to insert 4 * @return node's predecessor 5 * 6 * 同步器通过死循环的方式来保证节点的正确添加,在“死循环” 中通过CAS将节点设置成为尾节点之后,当前线程才能从该方法中返回,否则当前线程不断的尝试设置。 7 * enq方法将并发添加节点的请求通过CAS变得“串行化”了。 8 * 9 */ 10 private Node enq(final Node node) { 11 for (;;) { 12 Node t = tail; 13 if (t == null) { // Must initialize 14 if (compareAndSetHead(new Node())) 15 tail = head; 16 } else { 17 node.prev = t; 18 if (compareAndSetTail(t, node)) { 19 t.next = node; 20 return t; 21 } 22 } 23 } 24 }
acquireQueued方法:在队列中的线程获取锁的过程:
1 /** 2 * Acquires in exclusive uninterruptible mode for thread already in 3 * queue. Used by condition wait methods as well as acquire. 4 * 5 * @param node the node 6 * @param arg the acquire argument 7 * @return {@code true} if interrupted while waiting 8 * 9 * acquireQueued方法当前线程在死循环中获取同步状态,而只有前驱节点是头节点才能尝试获取同步状态(锁)( p == head && tryAcquire(arg)) 10 * 原因是:1.头结点是成功获取同步状态(锁)的节点,而头节点的线程释放了同步状态以后,将会唤醒其后继节点,后继节点的线程被唤醒后要检查自己的前驱节点是否为头结点。 11 * 2.维护同步队列的FIFO原则,节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说是每个线程)都在自省的观察。 12 * 13 */ 14 final boolean acquireQueued(final Node node, int arg) { 15 boolean failed = true; 16 try { 17 boolean interrupted = false; 18 //死循环检查(自旋检查)当前节点的前驱节点是否为头结点,才能获取锁 19 for (;;) { 20 // 获取节点的前驱节点 21 final Node p = node.predecessor(); 22 if (p == head && tryAcquire(arg)) {//节点中的线程循环的检查,自己的前驱节点是否为头节点 23 //将当前节点设置为头结点,移除之前的头节点 24 setHead(node); 25 p.next = null; // help GC 26 failed = false; 27 return interrupted; 28 } 29 // 否则检查前一个节点的状态,看当前获取锁失败的线程是否要挂起 30 if (shouldParkAfterFailedAcquire(p, node) && 31 //如果需要挂起,借助JUC包下面的LockSupport类的静态方法park挂起当前线程,直到被唤醒 32 parkAndCheckInterrupt()) 33 interrupted = true; 34 } 35 } finally { 36 //如果有异常 37 if (failed) 38 //取消请求,将当前节点从队列中移除 39 cancelAcquire(node); 40 } 41 }
独占式的获取同步状态的流程如下:
图5 独占式的获取同步状态的流程
7.独占锁的释放:下面直接看源码:
1 /* 2 1. unlock():unlock()是解锁函数,它是通过AQS的release()函数来实现的。 3 * 在这里,“1”的含义和“获取锁的函数acquire(1)的含义”一样,它是设置“释放锁的状态”的参数。 4 * 由于“公平锁”是可重入的,所以对于同一个线程,每释放锁一次,锁的状态-1。 5 6 unlock()在ReentrantLock.java中实现的,源码如下: 7 */ 8 public void unlock() { 9 sync.release(1); 10 }
release()会调用tryRelease方法尝试释放当前线程持有的锁(同步状态),成功的话唤醒后继线程,并返回true,否则直接返回false
1 /** 2 * Releases in exclusive mode. Implemented by unblocking one or 3 * more threads if {@link #tryRelease} returns true. 4 * This method can be used to implement method {@link Lock#unlock}. 5 * 6 * @param arg the release argument. This value is conveyed to 7 * {@link #tryRelease} but is otherwise uninterpreted and 8 * can represent anything you like. 9 * @return the value returned from {@link #tryRelease} 10 * 11 * 12 * 13 */ 14 public final boolean release(int arg) { 15 if (tryRelease(arg)) { 16 Node h = head; 17 if (h != null && h.waitStatus != 0) 18 unparkSuccessor(h); 19 return true; 20 } 21 return false; 22 }
1 // tryRelease() 尝试释放当前线程的同步状态(锁) 2 protected final boolean tryRelease(int releases) { 3 //c为释放后的同步状态 4 int c = getState() - releases; 5 //判断当前释放锁的线程是否为获取到锁(同步状态)的线程,不是抛出异常(非法监视器状态异常) 6 if (Thread.currentThread() != getExclusiveOwnerThread()) 7 throw new IllegalMonitorStateException(); 8 boolean free = false; 9 //如果锁(同步状态)已经被当前线程彻底释放,则设置锁的持有者为null,同步状态(锁)变的可获取 10 if (c == 0) { 11 free = true; 12 setExclusiveOwnerThread(null); 13 } 14 setState(c); 15 return free; 16 }
释放锁成功后,找到AQS的头结点,并唤醒它即可:
1 // 4. 唤醒头结点的后继节点 2 3 private void unparkSuccessor(Node node) { 4 //获取头结点(线程)的状态 5 int ws = node.waitStatus; 6 //如果状态<0,设置当前线程对应的锁的状态为0 7 if (ws < 0) 8 compareAndSetWaitStatus(node, ws, 0); 9 10 Node s = node.next; 11 12 //解释:Thread to unpark is held in successor, which is normally just the next node. 13 //But if cancelled or apparently(显然) null, traverse backwards(向后遍历) from tail to find the actual(实际的) non-cancelled successor(前继节点). 14 //从队列尾部开始往前去找最前面的一个waitStatus小于0的节点。 15 if (s == null || s.waitStatus > 0) { 16 s = null; 17 for (Node t = tail; t != null && t != node; t = t.prev) 18 if (t.waitStatus <= 0) 19 s = t; 20 } 21 //唤醒后继节点对应的线程 22 if (s != null) 23 LockSupport.unpark(s.thread); 24 }
上面说的是ReentrantLock的公平锁获取和释放的AQS的源码,唯独还剩下一个非公平锁NonfairSync没说,其实,它和公平锁的唯一区别就是获取锁的方式不同,公平锁是按前后顺序一次获取锁,非公平锁是抢占式的获取锁,那ReentrantLock中的非公平锁NonfairSync是怎么实现的呢?
1 /** 2 * Sync object for non-fair locks 3 */ 4 static final class NonfairSync extends Sync { 5 private static final long serialVersionUID = 7316153563782823691L; 6 7 /** 8 * Performs lock. Try immediate barge, backing up to normal 9 * acquire on failure. 10 */ 11 final void lock() { 12 if (compareAndSetState(0, 1)) 13 setExclusiveOwnerThread(Thread.currentThread()); 14 else 15 acquire(1); 16 } 17 18 protected final boolean tryAcquire(int acquires) { 19 return nonfairTryAcquire(acquires); 20 } 21 }
非公平锁的lock的时候多了上面加粗的代码:在lock的时候先直接用cas判断state变量是否为0(尝试获取锁),成功的话更新成1,表示当前线程获取到了锁,不需要在排队,从而直接抢占的目的。而对于公平锁的lock方法是一开始就走AQS的双向队列排队获取锁。更详细的关于ReentrantLock的实现请看后面写的一篇文章:http://www.cnblogs.com/200911/p/6035765.html
总结:在获取同步状态的时候,同步器维护一个同步队列,获取失败的线程会被加入到队列中并在队列中自旋;移除队列(或停止自旋)的条件是前驱节点为头结点并且获取到了同步状态。在释放同步状态时,同步器调用tryRelease(int args)方法释放同步状态,然后唤醒头结点的后继节点。AQS的实现思路其实并不复杂,用一句话准确的描述的话,其实就是使用标志状态位status(volatile int state)和 一个双向队列的入队和出队来实现。AQS维护一个线程何时访问的状态,它只是对状态负责,而这个状态的含义,子类可以自己去定义。
来源:CSDN
作者:布拉格的爱
链接:https://blog.csdn.net/zhangdongnihao/article/details/104029261