文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java中的AQS到底是什么?高级面试必问!

2024-12-03 18:47

关注

JDK1.5以前只有synchronized同步锁,并且效率非常低,因此大神Doug Lea自己写了一套并发框架,这套框架的核心就在于AbstractQueuedSynchronizer类(即AQS),性能非常高,所以被引入JDK包中,即JUC。

那么AQS是怎么实现的呢?

本篇就是对AQS及其相关组件进行分析,了解其原理,并领略大神的优美而又精简的代码。

AbstractQueuedSynchronizer

AQS是JUC下最核心的类,没有之一,所以我们先来分析一下这个类的数据结构。

AQS内部是使用了双向链表将等待线程链接起来,当发生并发竞争的时候,就会初始化该队列并让线程进入睡眠等待唤醒,同时每个节点会根据是否为共享锁标记状态为共享模式或独占模式。

这个数据结构需要好好理解并牢牢记住,下面分析的组件都将基于此实现。

Lock

Lock是一个接口,提供了加/解锁的通用API,JUC主要提供了两种锁,ReentrantLock和ReentrantReadWriteLock,前者是重入锁,实现Lock接口,后者是读写锁,本身并没有实现Lock接口,而是其内部类ReadLock或WriteLock实现了Lock接口。

先来看看Lock都提供了哪些接口: 

  1. // 普通加锁,不可打断;未获取到锁进入AQS阻塞    
  2. void lock();   
  3.  // 可打断锁    
  4. void lockInterruptibly() throws InterruptedException;    
  5.  // 尝试加锁,未获取到锁不阻塞,返回标识  
  6.  boolean tryLock();    
  7.  // 带超时时间的尝试加锁    
  8. boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    
  9.  // 解锁    
  10. void unlock();    
  11.  // 创建一个条件队列    
  12. Condition newCondition();   

看到这里读者们可以先思考下,自己如何来实现上面这些接口。

ReentrantLock

加锁

synchronized和ReentrantLock都是可重入的,后者使用更加灵活,也提供了更多的高级特性,但其本质的实现原理是差不多的(据说synchronized是借鉴了ReentrantLock的实现原理)。

ReentrantLock提供了两个构造方法: 

  1. public ReentrantLock() {    
  2.     sync = new NonfairSync();    
  3. }    
  4. public ReentrantLock(boolean fair) {    
  5.     sync = fair ? new FairSync() : new NonfairSync();    
  6. }   

有参构造是根据参数创建公平锁或非公平锁,而无参构造默认则是非公平锁,因为非公平锁性能非常高,并且大部分业务并不需要使用公平锁。

至于为什么非公平锁性能很高,咱们接着往下看。

非公平锁/公平锁

lock

非公平锁和公平锁在实现上基本一致,只有个别的地方不同,因此下面会采用对比分析方法进行分析。

从lock方法开始: 

  1. public void lock() {    
  2.     sync.lock();    
  3. }   

实际上是委托给了内部类Sync,该类实现了AQS(其它组件实现方法也基本上都是这个套路);由于有公平和非公平两种模式,因此该类又实现了两个子类:FairSync和NonfairSync: 

  1. // 非公平锁 
  2. final void lock() {    
  3.     if (compareAndSetState(0, 1)) 
  4.          setExclusiveOwnerThread(Thread.currentThread());    
  5.     else    
  6.         acquire(1);    
  7. }   
  8.  // 公平锁   
  9. final void lock() {   
  10.    acquire(1);   
  11. }   

这里就是公平锁和非公平锁的第一个不同,非公平锁首先会调用CAS将state从0改为1,如果能改成功则表示获取到锁,直接将exclusiveOwnerThread设置为当前线程,不用再进行后续操作;否则则同公平锁一样调用acquire方法获取锁,这个是在AQS中实现的模板方法: 

  1. public final void acquire(int arg) {    
  2.     if (!tryAcquire(arg) &&    
  3.         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))    
  4.         selfInterrupt();    
  5. }   

tryAcquire

这里两种锁唯一不同的实现就是tryAcquire方法,先来看非公平锁的实现: 

  1. protected final boolean tryAcquire(int acquires) {    
  2.     return nonfairTryAcquire(acquires);    
  3. }    
  4. final boolean nonfairTryAcquire(int acquires) {   
  5.      final Thread current = Thread.currentThread();    
  6.     int c = getState();    
  7.     if (c == 0) {    
  8.         if (compareAndSetState(0, acquires)) {  
  9.             setExclusiveOwnerThread(current);    
  10.             return true;    
  11.         }    
  12.     }    
  13.     else if (current == getExclusiveOwnerThread()) {    
  14.         int nextc = c + acquires;    
  15.         if (nextc < 0) // overflow    
  16.             throw new Error("Maximum lock count exceeded");    
  17.         setState(nextc);    
  18.         return true;    
  19.     }    
  20.     return false;    
  21. }   

state=0表示还没有被线程持有锁,直接通过CAS修改,能修改成功的就获取到锁,修改失败的线程先判断exclusiveOwnerThread是不是当前线程,是则state+1,表示重入次数+1并返回true,加锁成功,否则则返回false表示尝试加锁失败并调用acquireQueued入队。 

  1. protected final boolean tryAcquire(int acquires) {    
  2.     final Thread current = Thread.currentThread();    
  3.     int c = getState();    
  4.     if (c == 0) {    
  5.         if (!hasQueuedPredecessors() &&    
  6.             compareAndSetState(0, acquires)) {   
  7.             setExclusiveOwnerThread(current);    
  8.             return true;    
  9.         }    
  10.     }    
  11.     else if (current == getExclusiveOwnerThread()) {    
  12.         int nextc = c + acquires;    
  13.         if (nextc < 0)    
  14.             throw new Error("Maximum lock count exceeded");   
  15.         setState(nextc);    
  16.         return true;    
  17.     }    
  18.     return false;    
  19. }    
  20. public final boolean hasQueuedPredecessors() {    
  21.     Node t = tail; // Read fields in reverse initialization order  
  22.     Node h = head;    
  23.     Node s;    
  24.     // 首尾不相等且头结点线程不是当前线程则表示需要进入队列    
  25.     return h != t &&    
  26.         ((s = h.next) == null || s.thread != Thread.currentThread());   
  27. }   

上面就是公平锁的尝试获取锁的代码,可以看到基本和非公平锁的代码是一样的,区别在于首次加锁需要判断是否已经有队列存在,没有才去加锁,有则直接返回false。

addWaiter

接着来看addWaiter方法,当尝试加锁失败时,首先就会调用该方法创建一个Node节点并添加到队列中去。 

  1. private Node addWaiter(Node mode) {    
  2.     Node node = new Node(Thread.currentThread(), mode);    
  3.     Node pred = tail;    
  4.     // 尾节点不为null表示已经存在队列,直接将当前线程作为尾节点   
  5.     if (pred != null) {    
  6.         node.prev = pred;    
  7.         if (compareAndSetTail(pred, node)) {    
  8.             pred.next = node;    
  9.             return node;    
  10.         }    
  11.     }    
  12.     // 尾结点不存在则表示还没有初始化队列,需要初始化队列    
  13.     enq(node);    
  14.     return node;    
  15. }    
  16. private Node enq(final Node node) {    
  17. // 自旋    
  18.     for (;;) {    
  19.         Node t = tail;    
  20.         if (t == null) { // 只会有一个线程设置头节点成功     
  21.             if (compareAndSetHead(new Node()))    
  22.                 tail = head;    
  23.         } else { // 其它设置头节点失败的都会自旋设置尾节点    
  24.             node.prev = t;    
  25.             if (compareAndSetTail(t, node)) {   
  26.                  t.next = node;    
  27.                 return t;   
  28.              }    
  29.         }    
  30.     }    
  31. }   

这里首先传入了一个独占模式的空节点,并根据该节点和当前线程创建了一个Node,然后判断是否已经存在队列,若存在则直接入队,否则调用enq方法初始化队列,提高效率。

此处还有一个非常细节的地方,为什么设置尾节点时都要先将之前的尾节点设置为node.pre的值呢,而不是在CAS之后再设置?

比如像下面这样: 

  1. if (compareAndSetTail(pred, node)) {    
  2.  node.prev = pred;    
  3.     pred.next = node;    
  4.     return node;    
  5. }   

因为如果这样做的话,在CAS设置完tail后会存在一瞬间的tail.pre=null的情况,而Doug Lea正是考虑到这种情况,不论何时获取tail.pre都不会为null。

acquireQueued

接着看acquireQueued方法: 

  1. final boolean acquireQueued(final Node node, int arg) {    
  2.  // 为true表示存在需要取消加锁的节点,仅从这段代码可以看出,    
  3.  // 除非发生异常,否则不会存在需要取消加锁的节点。    
  4.     boolean failed = true;    
  5.     try {    
  6.      // 打断标记,因为调用的是lock方法,所以是不可打断的    
  7.      // (但实际上是打断了的,只不过这里采用了一种**静默**处理方式,稍后分析)    
  8.         boolean interrupted = false;    
  9.         for (;;) {    
  10.             final Node p = node.predecessor();  
  11.             if (p == head && tryAcquire(arg)) {    
  12.                 setHead(node);    
  13.                 p.next = null; // help GC    
  14.                 failed = false;    
  15.                 return interrupted;    
  16.             }   
  17.              if (shouldParkAfterFailedAcquire(p, node) &&    
  18.                 parkAndCheckInterrupt())    
  19.                 interrupted = true;    
  20.         }    
  21.     } finally {    
  22.         if (failed)    
  23.             cancelAcquire(node);    
  24.     }    
  25. }     
  26. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {   
  27.     int ws = pred.waitStatus;    
  28.     if (ws == Node.SIGNAL)    
  29.         return true;    
  30.     if (ws > 0) {    
  31.         do {    
  32.             node.prev = pred = pred.prev;    
  33.         } while (pred.waitStatus > 0);    
  34.         pred.next = node;    
  35.     } else {    
  36.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    
  37.     }    
  38.     return false;    
  39. }    
  40. private final boolean parkAndCheckInterrupt() {   
  41.     LockSupport.park(this);    
  42.     return Thread.interrupted();    
  43. }   

这里就是队列中线程加锁/睡眠的核心逻辑,首先判断刚刚调用addWaiter方法添加到队列的节点是否是头节点,如果是则再次尝试加锁,这个刚刚分析过了,非公平锁在这里就会再次抢一次锁,抢锁成功则设置为head节点并返回打断标记;否则则和公平锁一样调用shouldParkAfterFailedAcquire判断是否应该调用park方法进入睡眠。

park细节

为什么在park前需要这么一个判断呢?因为当前节点的线程进入park后只能被前一个节点唤醒,那前一个节点怎么知道有没有后继节点需要唤醒呢?

因此当前节点在park前需要给前一个节点设置一个标识,即将waitStatus设置为Node.SIGNAL(-1),然后自旋一次再走一遍刚刚的流程,若还是没有获取到锁,则调用parkAndCheckInterrupt进入睡眠状态。

打断

读者可能会比较好奇Thread.interrupted这个方法是做什么用的。 

  1. public static boolean interrupted() {    
  2.     return currentThread().isInterrupted(true);   

这个是用来判断当前线程是否被打断过,并清除打断标记(若是被打断过则会返回true,并将打断标记设置为false),所以调用lock方法时,通过interrupt也是会打断睡眠的线程的,只是Doug Lea做了一个假象,让用户无感知。

但有些场景又需要知道该线程是否被打断过,所以acquireQueued最终会返回interrupted打断标记,如果是被打断过,则返回的true,并在acquire方法中调用selfInterrupt再次打断当前线程(将打断标记设置为true)。

这里我们对比看看lockInterruptibly的实现: 

  1. public void lockInterruptibly() throws InterruptedException {    
  2.     sync.acquireInterruptibly(1);    
  3. }    
  4. public final void acquireInterruptibly(int arg)    
  5.         throws InterruptedException {    
  6.     if (Thread.interrupted())    
  7.         throw new InterruptedException();    
  8.     if (!tryAcquire(arg))    
  9.         doAcquireInterruptibly(arg);    
  10. }    
  11. private void doAcquireInterruptibly(int arg)    
  12.     throws InterruptedException {    
  13.     final Node node = addWaiter(Node.EXCLUSIVE);    
  14.     boolean failed = true;    
  15.     try {    
  16.         for (;;) {    
  17.             final Node p = node.predecessor();    
  18.             if (p == head && tryAcquire(arg)) {    
  19.                 setHead(node);    
  20.                 p.next = null; // help GC    
  21.                 failed = false;    
  22.                 return;    
  23.             }    
  24.             if (shouldParkAfterFailedAcquire(p, node) &&   
  25.                 parkAndCheckInterrupt())    
  26.                 throw new InterruptedException();    
  27.         }    
  28.     } finally {    
  29.         if (failed)    
  30.             cancelAcquire(node);    
  31.     }    
  32. }   

可以看到区别就在于使用lockInterruptibly加锁被打断后,是直接抛出InterruptedException异常,我们可以捕获这个异常进行相应的处理。

取消

最后来看看cancelAcquire是如何取消加锁的,该情况比较特殊,简单了解下即可: 

  1. private void cancelAcquire(Node node) {    
  2.     if (node == null)    
  3.         return;    
  4. // 首先将线程置空    
  5.     node.thread = null;    
  6. // waitStatus > 0表示节点处于取消状态,则直接将当前节点的pre指向在此之前的最后一个有效节点    
  7.     Node pred = node.prev;    
  8.     while (pred.waitStatus > 0)    
  9.         node.prev = pred = pred.prev;    
  10. // 保存前一个节点的下一个节点,如果在此之前存在取消节点,这里就是之前取消被取消节点的头节点   
  11.     Node predpredNext = pred.next;   
  12.     node.waitStatus = Node.CANCELLED;    
  13. // 当前节点是tail节点,则替换尾节点,替换成功则将新的尾结点的下一个节点设置为null;    
  14. // 否则需要判断是将当前节点的下一个节点赋值给最后一个有效节点,还是唤醒下一个节点。    
  15.     if (node == tail && compareAndSetTail(node, pred)) {    
  16.         compareAndSetNext(pred, predNext, null);    
  17.     } else {    
  18.         int ws;    
  19.         if (pred != head &&    
  20.             ((ws = pred.waitStatus) == Node.SIGNAL ||    
  21.              (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&    
  22.             pred.thread != null) {    
  23.             Node next = node.next;    
  24.             if (next != null && next.waitStatus <= 0)    
  25.                 compareAndSetNext(pred, predNext, next);    
  26.         } else {    
  27.             unparkSuccessor(node);    
  28.         }    
  29.         nodenode.next = node; // help GC    
  30.     }    
  31. }   

解锁 

  1. public void unlock() {    
  2.     sync.release(1);    
  3. }    
  4. public final boolean release(int arg) {    
  5.     if (tryRelease(arg)) {    
  6.         Node h = head;    
  7.         if (h != null && h.waitStatus != 0)    
  8.             unparkSuccessor(h);    
  9.         return true;    
  10.     }    
  11.     return false;    
  12. }     
  13. protected final boolean tryRelease(int releases) {    
  14.     int c = getState() - releases;    
  15.     if (Thread.currentThread() != getExclusiveOwnerThread())    
  16.         throw new IllegalMonitorStateException();    
  17.     boolean free = false;    
  18.     if (c == 0) {    
  19.         free = true;    
  20.         setExclusiveOwnerThread(null);    
  21.     }    
  22.     setState(c);    
  23.     return free;    
  24. }    
  25. private void unparkSuccessor(Node node) {    
  26.     int ws = node.waitStatus;    
  27.     if (ws < 0)    
  28.         compareAndSetWaitStatus(node, ws, 0);    
  29.     Node s = node.next;    
  30.     // 并发情况下,可能已经被其它线程唤醒或已经取消,则从后向前找到最后一个有效节点并唤醒   
  31.     if (s == null || s.waitStatus > 0) {    
  32.         s = null;    
  33.         for (Node t = tail; t != null && t != node; tt = t.prev)    
  34.             if (t.waitStatus <= 0)    
  35.                 s = t;    
  36.     }    
  37.     if (s != null)    
  38.         LockSupport.unpark(s.thread);    
  39. }   

解锁就比较简单了,先调用tryRelease对state执行减一操作,如果state==0,则表示完全释放锁;若果存在后继节点,则调用unparkSuccessor唤醒后继节点,唤醒后的节点的waitStatus会重新被设置为0。

只是这里有一个小细节,为什么是从后向前找呢?因为我们在开始说过,设置尾节点保证了node.pre不会为null,但pre.next仍有可能是null,所以这里只能从后向前找到最后一个有效节点。

小结

上面是ReentrantLock的加锁流程,可以看到整个流程不算复杂,只是判断和跳转比较多,主要是Doug Lea将代码和性能都优化到了极致,代码非常精简,但细节却非常多。

另外通过上面的分析,我们也可以发现,公平锁和非公平锁的区别就在于非公平锁不管是否有线程在排队,先抢三次锁,而公平锁则会判断是否存在队列,有线程在排队则直接进入队列排队;另外线程在park被唤醒后非公平锁还会抢锁,公平锁仍然需要排队,所以非公平锁的性能比公平锁高很多,大部分情况下我们使用非公平锁即可。

ReentrantReadWriteLock

ReentrantLock是一把独占锁,只支持重入,不支持共享,所以JUC包下还提供了读写锁,这把锁支持读读并发,但读写、写写都是互斥的。

读写锁也是基于AQS实现的,也包含了一个继承自AQS的内部类Sync,同样也有公平和非公平两种模式,下面主要讨论非公平模式下的读写锁实现。

读写锁实现相对比较复杂,在ReentrantLock中就是使用的int型的state属性来表示锁被某个线程占有和重入次数,而ReentrantReadWriteLock分为了读和写两种锁,要怎么用一个字段表示两种锁的状态呢?

Doug Lea大师将state字段分为了高二字节和低二字节,即高16位用来表示读锁状态,低16位则用来表示写锁,如下图:

因为读写锁状态都只用了两个字节,所以可重入的次数最多是65535,当然正常情况下重入是不可能达到这么多的。

那它是怎么实现的呢?还是先从构造方法开始: 

  1. public ReentrantReadWriteLock() {    
  2.     this(false);    
  3. }   
  4. public ReentrantReadWriteLock(boolean fair) {    
  5.     sync = fair ? new FairSync() : new NonfairSync();    
  6.     readerLock = new ReadLock(this);    
  7.     writerLock = new WriteLock(this);    
  8. }   

同样默认就是非公平锁,同时还创建了readerLock和writerLock两个对象,我们只需要像下面这样就能获取到读写锁: 

  1. private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();    
  2. private static Lock r = lock.readLock();    
  3. private static Lock w = lock.writeLock();   

写锁

由于写锁的加锁过程相对更简单,下面先从写锁加锁开始分析,入口在ReentrantReadWriteLock#WriteLock.lock()方法,点进去看,发现还是使用的AQS中的acquire方法: 

  1. public final void acquire(int arg) {    
  2.     if (!tryAcquire(arg) &&    
  3.         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))    
  4.         selfInterrupt();    
  5. }   

所以不同的地方也只有tryAcquire方法,我们重点分析这个方法就行: 

  1. static final int SHARED_SHIFT   = 16;    
  2. // 65535    
  3. static final int MAX\_COUNT      = (1 << SHARED\_SHIFT) - 1;    
  4. // 低16位是1111....1111    
  5. static final int EXCLUSIVE\_MASK = (1 << SHARED\_SHIFT) - 1;    
  6. // 得到c低16位的值    
  7. static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }    
  8. protected final boolean tryAcquire(int acquires) {    
  9.     Thread current = Thread.currentThread();    
  10.     int c = getState();    
  11.     // 获取写锁加锁和重入的次数    
  12.     int w = exclusiveCount(c);   
  13.      if (c != 0) { // 已经有线程持有锁    
  14.      // 这里有两种情况:1. c!=0 && w==0表示有线程获取了读锁,不论是否是当前线程,直接返回false,  
  15.      // 也就是说读-写锁是不支持升级重入的(但支持写-读降级),原因后文会详细分析;    
  16.      // 2. c!=0 && w!=0 && current != getExclusiveOwnerThread()表示有其它线程持有了写锁,写写互斥    
  17.         if (w == 0 || current != getExclusiveOwnerThread())    
  18.             return false;    
  19. // 超出65535,抛异常    
  20.         if (w + exclusiveCount(acquires) > MAX_COUNT) 
  21.              throw new Error("Maximum lock count exceeded");    
  22.         // 否则写锁的次数直接加1    
  23.         setState(c + acquires);    
  24.         return true;    
  25.     }    
  26. // c==0才会走到这,但这时存在两种情况,有队列和无队列,所以公平锁和非公平锁处理不同,    
  27. // 前者需要判断是否存在队列,有则尝试加锁失败,无则加锁成功,而非公平锁直接使用CAS加锁即可    
  28.     if (writerShouldBlock() ||    
  29.         !compareAndSetState(c, c + acquires))    
  30.         return false;    
  31.     setExclusiveOwnerThread(current);    
  32.     return true;    
  33. }   

写锁尝试加锁的过程就分析完了,其余的部分上文已经讲过,这里不再赘述。

读锁 

  1. public void lock() {    
  2.     sync.acquireShared(1);    
  3. }    
  4. public final void acquireShared(int arg) {    
  5.     if (tryAcquireShared(arg) < 0)    
  6.         doAcquireShared(arg);   
  7. }    

读锁在加锁开始就和其它锁不同,调用的是acquireShared方法,意为获取共享锁。 

  1. static final int SHARED\_UNIT    = (1 << SHARED\_SHIFT);    
  2. // 右移16位得到读锁状态的值    
  3. static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }    
  4. protected final int tryAcquireShared(int unused) {    
  5.      Thread current = Thread.currentThread();    
  6.      int c = getState();    
  7.      // 为什么读写互斥?因为读锁一上来就判断了是否有其它线程持有了写锁(当前线程持有写锁再获取读锁是可以的)  
  8.      if (exclusiveCount(c) != 0 &&    
  9.          getExclusiveOwnerThread() != current)    
  10.          return -1;    
  11.      int r = sharedCount(c);    
  12.      // 公平锁判断是否存在队列,非公平锁判断第一个节点是不是EXCLUSIVE模式,是的话会返回true 
  13.       // 返回false则需要判断读锁加锁次数是否超过65535,没有则使用CAS给读锁+1    
  14.      if (!readerShouldBlock() &&    
  15.          r < MAX_COUNT &&    
  16.          compareAndSetState(c, c + SHARED_UNIT)) {    
  17.          if (r == 0) {    
  18.           // 第一个读锁线程就是当前线程    
  19.              firstReader = current;    
  20.              firstReaderHoldCount = 1;    
  21.          } else if (firstReader == current) {  
  22.           // 记录读锁的重入    
  23.              firstReaderHoldCount++;    
  24.          } else {    
  25.           // 获取最后一次加读锁的重入次数记录器HoldCounter    
  26.              HoldCounter rh = cachedHoldCounter;    
  27.              if (rh == null || rh.tid != getThreadId(current))    
  28.               // 当前线程第一次重入需要初始化,以及当前线程和缓存的最后一次记录器的线程id不同,需要从ThreadLocalHoldCounter拿到对应的记录器    
  29.                  cachedHoldCounter = rh = readHolds.get();    
  30.              else if (rh.count == 0)    
  31.               // 缓存到ThreadLocal    
  32.                  readHolds.set(rh);  
  33.               rh.count++;    
  34.          }    
  35.          return 1;    
  36.      }    
  37.      return fullTryAcquireShared(current);    
  38. }   

这段代码有点复杂,首先需要保证读写互斥,然后进行初次加锁,若加锁失败就会调用fullTryAcquireShared方法进行兜底处理。在初次加锁中与写锁不同的是,写锁的state可以直接用来记录写锁的重入次数,因为写写互斥,但读锁是共享的,state用来记录读锁的加锁次数了,重入次数该怎么记录呢?

重入是指同一线程,那么是不是可以使用ThreadLocl来保存呢?没错,Doug Lea就是这么处理的,新增了一个HoldCounter类,这个类只有线程id和重入次数两个字段,当线程重入的时候就会初始化这个类并保存在ThreadLocalHoldCounter类中,这个类就是继承ThreadLocl的,用来初始化HoldCounter对象并保存。

这里还有个小细节,为什么要使用cachedHoldCounter缓存最后一次加读锁的HoldCounter?

因为大部分情况下,重入和释放锁的线程很有可能就是最后一次加锁的线程,所以这样做能够提高加解锁的效率,Doug Lea真是把性能优化到了极致。

上面只是初次加锁,有可能会加锁失败,就会进入到fullTryAcquireShared方法: 

  1. final int fullTryAcquireShared(Thread current) {    
  2.     HoldCounter rh = null;    
  3.     for (;;) {    
  4.         int c = getState();    
  5.         if (exclusiveCount(c) != 0) {    
  6.             if (getExclusiveOwnerThread() != current)   
  7.                 return -1;    
  8.         } else if (readerShouldBlock()) {    
  9.             if (firstReader == current) {    
  10.                 // assert firstReaderHoldCount > 0;    
  11.             } else {    
  12.                 if (rh == null) {    
  13.                     rh = cachedHoldCounter;    
  14.                     if (rh == null || rh.tid != getThreadId(current)) {    
  15.                         rh = readHolds.get();   
  16.                          if (rh.count == 0)    
  17.                             readHolds.remove();    
  18.                     }    
  19.                 }    
  20.                 if (rh.count == 0)    
  21.                     return -1;   
  22.              }    
  23.         }    
  24.         if (sharedCount(c) == MAX_COUNT)    
  25.             throw new Error("Maximum lock count exceeded");    
  26.         if (compareAndSetState(c, c + SHARED_UNIT)) {    
  27.             if (sharedCount(c) == 0) {   
  28.                  firstReader = current;    
  29.                 firstReaderHoldCount = 1;    
  30.             } else if (firstReader == current) {    
  31.                 firstReaderHoldCount++;    
  32.             } else {    
  33.                 if (rh == null)    
  34.                     rh = cachedHoldCounter;   
  35.                 if (rh == null || rh.tid != getThreadId(current))   
  36.                     rh = readHolds.get();    
  37.                 else if (rh.count == 0)    
  38.                     readHolds.set(rh);    
  39.                 rh.count++;    
  40.                 cachedHoldCounter = rh; // cache for release    
  41.             }    
  42.             return 1;    
  43.         }    
  44.     }    
  45. }   

这个方法中代码和tryAcquireShared基本上一致,只是采用了自旋的方式,处理初次加锁中的漏网之鱼,读者们可自行阅读分析。

上面两个方法若返回大于0则表示加锁成功,小于0则会调用doAcquireShared方法,这个就和之前分析的acquireQueued差不多了: 

  1. private void doAcquireShared(int arg) {   
  2.  // 先添加一个SHARED类型的节点到队列    
  3.     final Node node = addWaiter(Node.SHARED); 
  4.      boolean failed = true;    
  5.     try {    
  6.         boolean interrupted = false;    
  7.         for (;;) {    
  8.             final Node p = node.predecessor();    
  9.             if (p == head) {    
  10.              // 再次尝试加读锁    
  11.                 int r = tryAcquireShared(arg);    
  12.                 if (r >= 0) {    
  13.                  // 设置head节点以及传播唤醒后面的读线程    
  14.                     setHeadAndPropagate(node, r);    
  15.                     p.next = null; // help GC    
  16.                     if (interrupted)    
  17.                         selfInterrupt();    
  18.                     failed = false;    
  19.                     return;   
  20.                 }    
  21.             }    
  22.             // 只有前一个节点的waitStatus=-1时才会park,=0或者-3(先不考虑-2和1的情况)都会设置为-1后再次自旋尝试加锁,若还是加锁失败就会park   
  23.             if (shouldParkAfterFailedAcquire(p, node) &&    
  24.                 parkAndCheckInterrupt())    
  25.                 interrupted = true;    
  26.         }    
  27.     } finally {    
  28.         if (failed)   
  29.              cancelAcquire(node);    
  30.     }    
  31. }    
  32. private void setHeadAndPropagate(Node node, int propagate) {    
  33.  // 设置头节点    
  34.     Node h = head; // Record old head for check below    
  35.     setHead(node);    
  36.     // propagate是tryAcquireShared的返回值,当前线程加锁成功还要去唤醒后继的共享节点    
  37.     // (其余的判断比较复杂,笔者也还未想明白,知道的读者可以指点一下)    
  38.     if (propagate > 0 || h == null || h.waitStatus < 0 ||    
  39.         (h = head) == null || h.waitStatus < 0) {    
  40.         Node s = node.next;    
  41.         // 判断后继节点是否是共享节点    
  42.         if (s == null || s.isShared())    
  43.             doReleaseShared();    
  44.     }    
  45. }    
  46. private void doReleaseShared() {    
  47.     for (;;) {    
  48.         Node h = head;    
  49.         // 存在后继节点    
  50.         if (h != null && h != tail) {    
  51.             int ws = h.waitStatus;    
  52.             if (ws == Node.SIGNAL) {    
  53.              // 当前一个节点加锁成功后自然需要将-1改回0,并唤醒后继线程,同时自旋将0改为-2让唤醒传播下去   
  54.                 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))    
  55.                     continue;            
  56.                 unparkSuccessor(h);    
  57.             }    
  58.             // 设置头节点的waitStatus=-2,使得唤醒可以传播下去   
  59.             else if (ws == 0 &&    
  60.                      !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))    
  61.                 continue;                 
  62.         }    
  63.         if (h == head)           
  64.             break;    
  65.     }    
  66. }    
  67. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {    
  68.     int ws = pred.waitStatus;    
  69.     if (ws == Node.SIGNAL)    
  70.         return true;    
  71.     if (ws > 0) {    
  72.         do {   
  73.             node.prev = pred = pred.prev;    
  74.         } while (pred.waitStatus > 0);    
  75.         pred.next = node;    
  76.     } else {    
  77.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    
  78.     }    
  79.     return false;   
  80. }   

这里的逻辑也非常的绕,当多个线程同时调用addWaiter添加到队列中后,并且假设这些节点的第一个节点的前一个节点就是head节点,那么第一个节点就能加锁成功(假设都是SHARED节点),其余的节点在第一个节点设置头节点之前都会进入shouldParkAfterFailedAcquire方法,这时候waitStatus都等于0,所以继续自旋不会park,若再次加锁还失败就会park(因为这时候waitStatus=-1),但都是读线程的情况下一般都不会出现,因为setHeadAndPropagate第一步就是修改head,所以其余SHARED节点最终都能加锁成功并一直将唤醒传播下去。

以上就是读写锁加锁过程,解锁比较简单,这里就不详细分析了。

小结

读写锁将state分为了高二字节和低二字节,分别存储读锁和写锁的状态,实现更为的复杂,在使用上还有几点需要注意:

  1. private static void rw() {    
  2.     r.lock();    
  3.     try {    
  4.         log.info("获取到读锁");    
  5.         w.lock();    
  6.         try {    
  7.             log.info("获取到写锁");    
  8.         } finally {    
  9.             w.unlock();    
  10.         }    
  11.     } finally {    
  12.         r.unlock();    
  13.     }    
  14. }   

多个线程访问都能获取到读锁,但读写互斥,彼此都要等待对方的读锁释放才能获取到写锁,这就造成了死锁。

ReentrantReadWriteLock在某些场景下性能上不算高,因此Doug Lea在JDK1.8的时候又提供了一把高性能的读写锁StampedLock,前者读写锁都是悲观锁,而后者提供了新的模式——乐观锁,但它不是基于AQS实现的,本文不进行分析。

Condition

Lock接口中还有一个方法newCondition,这个方法就是创建一个条件队列: 

  1. public Condition newCondition() {    
  2.     return sync.newCondition();    
  3. }    
  4. final ConditionObject newCondition() {    
  5.     return new ConditionObject();    
  6. }   

所谓条件队列就是创建一个新的ConditionObject对象,这个对象的数据结构在开篇就看过了,包含首、尾两个节点字段,每当调用Condition#await方法时就会在对应的Condition对象中排队等待: 

  1. public final void await() throws InterruptedException {    
  2.     if (Thread.interrupted())    
  3.         throw new InterruptedException();    
  4.     // 加入条件队列    
  5.     Node node = addConditionWaiter();    
  6.     // 因为Condition.await必须配合Lock.lock使用,所以await时就是将已获得锁的线程全部释放掉    
  7.     int savedState = fullyRelease(node);    
  8.     int interruptMode = 0;    
  9.     // 判断是在同步队列还是条件队列,后者则直接park   
  10.     while (!isOnSyncQueue(node)) {    
  11.         LockSupport.park(this);    
  12.         // 获取打断处理方式(抛出异常或重设标记)    
  13.         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)    
  14.             break;    
  15.     }    
  16.     // 调用aqs的方法    
  17.     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)    
  18.         interruptMode = REINTERRUPT;    
  19.     if (node.nextWaiter != null) // clean up if cancelled    
  20.      // 清除掉已经进入同步队列的节点    
  21.         unlinkCancelledWaiters();    
  22.     if (interruptMode != 0)    
  23.         reportInterruptAfterWait(interruptMode);    
  24. }    
  25. private Node addConditionWaiter() {    
  26.     Node t = lastWaiter;    
  27.     // 清除状态为取消的节点    
  28.     if (t != null && t.waitStatus != Node.CONDITION) {  
  29.         unlinkCancelledWaiters();    
  30.         t = lastWaiter;    
  31.     }    
  32. // 创建一个CONDITION状态的节点并添加到队列末尾    
  33.     Node node = new Node(Thread.currentThread(), Node.CONDITION);    
  34.     if (t == null)    
  35.         firstWaiter = node;    
  36.     else    
  37.         t.nextWaiter = node;    
  38.     lastWaiter = node;    
  39.     return node;    
  40. }   

await方法实现比较简单,大部分代码都是上文分析过的,这里不再重复。接着来看signal方法: 

  1. public final void signal() {    
  2.     if (!isHeldExclusively())    
  3.         throw new IllegalMonitorStateException();    
  4.     // 从条件队列第一个节点开始唤醒    
  5.     Node first = firstWaiter;    
  6.     if (first != null)    
  7.         doSignal(first);    
  8. }    
  9. private void doSignal(Node first) {    
  10.     do {    
  11.         if ( (firstfirstWaiter = first.nextWaiter) == null)    
  12.             lastWaiter = null;    
  13.         first.nextWaiter = null;    
  14.     } while (!transferForSignal(first) &&    
  15.              (first = firstWaiter) != null);    
  16. }    
  17. final boolean transferForSignal(Node node) {    
  18.  // 修改waitStatus状态,如果修改失败,则说明该节点已经从条件队列转移到了同步队列    
  19.     if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))    
  20.         return false;    
  21. // 上面修改成功,则将该节点添加到同步队列末尾,并返回之前的尾结点    
  22.     Node p = enq(node);    
  23.     int ws = p.waitStatus;    
  24.     if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))    
  25.      // unpark当前线程,结合await方法看    
  26.         LockSupport.unpark(node.thread);   
  27.     return true;    
  28. }    

signal的逻辑也比较简单,就是唤醒条件队列中的第一个节点,主要是要结合await的代码一起理解。

其它组件

上文分析的锁都是用来实现并发安全控制的,而对于多线程协作JUC又基于AQS提供了CountDownLatch、CyclicBarrier、Semaphore等组件,下面一一分析。

CountDownLatch

CountDownLatch在创建的时候就需要指定一个计数:

  1. CountDownLatch countDownLatch = new CountDownLatch(5);   

然后在需要等待的地方调用countDownLatch.await()方法,然后在其它线程完成任务后调用countDownLatch.countDown()方法,每调用一次该计数就会减一,直到计数为0时,await的地方就会自动唤醒,继续后面的工作,所以CountDownLatch适用于一个线程等待多个线程的场景,那它是怎么实现的呢?

读者们可以结合上文自己先思考下。 

  1. public CountDownLatch(int count) {    
  2.     if (count < 0) throw new IllegalArgumentException("count < 0");    
  3.     this.sync = new Sync(count);    
  4. }    
  5. Sync(int count) {    
  6.     setState(count);    
  7. }   

与前面讲的锁一样,也有一个内部类Sync继承自AQS,并且在构造时就将传入的计数设置到了state属性,看到这里不难猜到CountDownLatch的实现原理了。 

  1. public void await() throws InterruptedException {    
  2.     sync.acquireSharedInterruptibly(1);    
  3. }    
  4. public final void acquireSharedInterruptibly(int arg)    
  5.         throws InterruptedException {    
  6.     if (Thread.interrupted())    
  7.         throw new InterruptedException();    
  8.     if (tryAcquireShared(arg) < 0)    
  9.         doAcquireSharedInterruptibly(arg);    
  10. }   
  11. protected int tryAcquireShared(int acquires) {    
  12.     return (getState() == 0) ? 1 : -1;    
  13. }     

在await方法中使用的是可打断的方式获取的共享锁,同样除了tryAcquireShared方法,其余的都是复用的之前分析过的代码,而tryAcquireShared就是判断state是否等于0,不等于就阻塞。 

  1. public void countDown() {   
  2.     sync.releaseShared(1);    
  3. }    
  4. public final boolean releaseShared(int arg) {    
  5.     if (tryReleaseShared(arg)) {    
  6.         doReleaseShared();    
  7.         return true;    
  8.     }    
  9.     return false;   
  10.  }    
  11. protected boolean tryReleaseShared(int releases) {    
  12.     for (;;) {    
  13.         int c = getState();    
  14.         if (c == 0)    
  15.             return false;    
  16.         int nextc = c-1;    
  17.         if (compareAndSetState(c, nextc))    
  18.             return nextc == 0;    
  19.     }    
  20. }   

而调用countDown就更简单了,每次对state递减,直到为0时才会调用doReleaseShared释放阻塞的线程。

最后需要注意的是CountDownLatch的计数是不支持重置的,每次使用都要新建一个。这篇《倒计时器CountDownLatch》舍得看一下。

CyclicBarrier

CyclicBarrier和CountDownLatch使用差不多,不过它只有await方法。CyclicBarrier在创建时同样需要指定一个计数,当调用await的次数达到计数时,所有线程就会同时唤醒,相当于设置了一个“起跑线”,需要等所有运动员都到达这个“起跑线”后才能一起开跑。

另外它还支持重置计数,提供了reset方法。 

  1. public CyclicBarrier(int parties) {    
  2.     this(parties, null);    
  3. }    
  4. public CyclicBarrier(int parties, Runnable barrierAction) {    
  5.     if (parties <= 0) throw new IllegalArgumentException();    
  6.     this.parties = parties;    
  7.     this.count = parties;    
  8.     this.barrierCommand = barrierAction;    
  9. }   

CyclicBarrier提供了两个构造方法,我们可以传入一个Runnable类型的回调函数,当达到计数时,由最后一个调用await的线程触发执行。 

  1. public int await() throws InterruptedException, BrokenBarrierException {    
  2.     try {    
  3.         return dowait(false, 0L);    
  4.     } catch (TimeoutException toe) {   
  5.         throw new Error(toe); // cannot happen    
  6.     }    
  7. }   
  8. private int dowait(boolean timed, long nanos)    
  9.     throws InterruptedException, BrokenBarrierException,    
  10.            TimeoutException {    
  11.     final ReentrantLock lock = this.lock;    
  12.     lock.lock();    
  13.     try {    
  14.         final Generation g = generation 
  15.         if (g.broken)    
  16.             throw new BrokenBarrierException();    
  17. // 是否打断,打断会唤醒所有条件队列中的线程    
  18.         if (Thread.interrupted()) {    
  19.             breakBarrier();    
  20.             throw new InterruptedException();    
  21.         }    
  22. // 计数为0时,唤醒条件队列中的所有线程    
  23.         int index = --count;    
  24.         if (index == 0) {  // tripped    
  25.             boolean ranAction = false;    
  26.             try {    
  27.                 final Runnable command = barrierCommand;    
  28.                 if (command != null)    
  29.                     command.run();    
  30.                 ranAction = true;    
  31.                 nextGeneration();    
  32.                 return 0;    
  33.             } finally {    
  34.                 if (!ranAction)   
  35.                     breakBarrier();   
  36.             }    
  37.         }    
  38.         for (;;) {    
  39.             try {    
  40.              // 不带超时时间直接进入条件队列等待    
  41.                 if (!timed)    
  42.                     trip.await();    
  43.                 else if (nanos > 0L)    
  44.                     nanos = trip.awaitNanos(nanos);    
  45.             } catch (InterruptedException ie) {    
  46.                 if (g == generation && ! g.broken) {    
  47.                     breakBarrier();    
  48.                     throw ie;    
  49.                 } else {    
  50.                     Thread.currentThread().interrupt();    
  51.                 }    
  52.             }    
  53.             if (g.broken)    
  54.                 throw new BrokenBarrierException();    
  55.             if (g != generation)    
  56.                 return index;    
  57.             if (timed && nanos <= 0L) {    
  58.                 breakBarrier();    
  59.                 throw new TimeoutException();    
  60.             }    
  61.         }    
  62.     } finally {    
  63.         lock.unlock();   
  64.     }    
  65. }    
  66. private void nextGeneration() {    
  67.     // signal completion of last generation    
  68.     trip.signalAll();    
  69.     // set up next generation    
  70.     count = parties;    
  71.     generation = new Generation();    
  72. }   

这里逻辑比较清晰,就是使用了ReentrantLock以及Condition来实现。在构造方法中我们可以看到保存了两个变量count和parties,每次调用await都会对count变量递减,count不为0时都会进入到trip条件队列中等待,否则就会通过signalAll方法唤醒所有的线程,并将parties重新赋值给count。

reset方法很简单,这里不详细分析了。

Semaphore

Semaphore是信号的意思,或者说许可,可以用来控制最大并发量。初始定义好有几个信号,然后在需要获取信号的地方调用acquire方法,执行完成后,需要调用release方法回收信号。 

  1. public Semaphore(int permits) {    
  2.     sync = new NonfairSync(permits);   
  3. }   
  4. public Semaphore(int permits, boolean fair) {    
  5.     sync = fair ? new FairSync(permits) : new NonfairSync(permits);    
  6. }   

它也有两个构造方法,可以指定公平或是非公平,而permits就是state的值。 

  1. public void acquire() throws InterruptedException {    
  2.     sync.acquireSharedInterruptibly(1);    
  3. }    
  4. // 非公平方式    
  5. final int nonfairTryAcquireShared(int acquires) {    
  6.     for (;;) {    
  7.         int available = getState();    
  8.         int remaining = available - acquires;    
  9.         if (remaining < 0 ||    
  10.             compareAndSetState(available, remaining))    
  11.             return remaining;   
  12.      }    
  13. }    
  14. // 公平方式    
  15. protected int tryAcquireShared(int acquires) {    
  16.     for (;;) {    
  17.         if (hasQueuedPredecessors())    
  18.             return -1;    
  19.         int available = getState();   
  20.         int remaining = available - acquires;    
  21.         if (remaining < 0 ||    
  22.             compareAndSetState(available, remaining))    
  23.             return remaining;    
  24.     }    
  25. }   

acquire方法和CountDownLatch是一样的,只是tryAcquireShared区分了公平和非公平方式。获取到信号相当于加共享锁成功,否则则进入队列阻塞等待;而release方法和读锁解锁方式也是一样的,只是每次release都会将state+1。

总结

本文详细分析了AQS的核心原理、锁的实现以及常用的相关组件,掌握其原理能让我们准确的使用JUC下面的锁以及线程协作组件。

另外AQS代码设计是非常精良的,有非常多的细节,精简的代码中把所有的情况都考虑到了,细细体味对我们自身编码能力也会有很大的提高。

文章错误和不清楚的地方欢迎批评指出,另外超时相关的API本文都未涉及到,读者可自行分析。 

来源:Java技术栈内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯