前言
AbstractQueuedSynchronizer类如其名,抽象的队列式的同步器,通常被称之为AQS的类,它是一个非常有用的父类,可用来定义锁以及依赖于排队阻塞线程的其他同步器;ReentrantLock,ReentrantReadWriteLock,CountDownLatch,CyclicBarrier和Semaphore等这些类都是基于AQS类实现的。AbstractQueuedLongSynchronizer 类提供相同的功能但扩展了对同步状态的 64 位的支持。两者都扩展了类 AbstractOwnableSynchronizer(一个帮助记录当前保持独占同步的线程的简单类)。
框架
这个类的框架如图所示:
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
1. AQS用的是一个32位的整型来表示同步状态的,它是用volatile修饰的:
private volatile int state;
在互斥锁中它表示着线程是否已经获取了锁,0未获取,1已经获取了,大于1表示重入数。同时AQS提供了getState()、setState()、compareAndSetState()方法来获取和修改该值:
可重入锁指的是在一个线程中可以多次获取同一把锁,比如:
一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。synchronized也可以看做重入锁
所以可重入数大于1表示该线程可能调用了多个需要当前锁的方法,或同一个线程调用了多次lock()方法。
1 protected final int getState() { 2 return state; 3 } 4
5 protected final void setState(int newState) { 6 state = newState; 7 } 8
9 protected final boolean compareAndSetState(int expect, int update) { 10 return unsafe.compareAndSwapInt(this, stateOffset, expect, update); 11 }
这些方法需要java.util.concurrent.atomic包的支持,采用CAS操作,保证其原子性和可见性。
2. AQS内部维护着一个FIFO的CLH队列,所以AQS并不支持基于优先级的同步策略。至于为何要选择CLH队列,主要在于CLH锁相对于MSC锁,他更加容易处理cancel和timeout,同时他具备进出队列快、无所、畅通无阻、检查是否有线程在等待也非常容易(head != tail,头尾指针不同)。当然相对于原始的CLH队列锁,AQS采用的是一种变种的CLH队列锁:
1、原始CLH使用的locked自旋,而AQS的CLH则是在每个node里面使用一个状态字段来控制阻塞,而不是自旋。
2、为了可以处理timeout和cancel操作,每个node维护一个指向前驱的指针。如果一个node的前驱被cancel,这个node可以前向移动使用前驱的状态字段。
3、head结点使用的是傀儡结点。
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch),即我们常说的” 独占锁” 和 “共享锁”。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
源码解析
我们使用 ReentrantLock 时,可以通过 lock() 方法获取到锁,但是这并是 ReentrantLock 类所做的事,而是在底层调用了AQS的方法来完成的。所以我们有必要深入去了解 AQS 是怎样实现对锁的管理,保证线程的逻辑正确执行的。
我们分别对独占锁的管理流程 acquire-release 和 共享锁管理流程 acquireShared-releaseShare 进行分析。
acquire(int)
此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:
1 public final void acquire(int arg) { 2 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 3 selfInterrupt(); 4 }
执行流程:
- “当前线程”首先通过tryAcquire()尝试获取锁。获取成功的话,直接返回;尝试失败的话,进入到等待队列排序等待(前面还有可能有需要线程在等待该锁)。
- “当前线程”尝试失败的情况下,先通过addWaiter(Node.EXCLUSIVE)来将“当前线程”加入到”CLH队列(非阻塞的FIFO队列)”末尾。CLH队列就是线程等待队列。
- 再执行完addWaiter(Node.EXCLUSIVE)之后,会调用acquireQueued()来获取锁。由于此时ReentrantLock是公平锁,它会根据公平性原则来获取锁。
- “当前线程”在执行acquireQueued()时,会进入到CLH队列中休眠等待,直到获取锁了才返回!如果”当前线程”在休眠等待过程中被中断过,acquireQueued会返回true,此时”当前线程”会调用selfInterrupt()来自己给自己产生一个中断。至于为什么要自己给自己产生一个中断,后面再介绍。
所以我们对这里面的方法一个一个介绍
1、tryAcquire(int)
此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义,还是那句话,当然不仅仅只限于tryLock()。如下是tryAcquire()的源码:
1 protected boolean tryAcquire(int arg) { 2 throw new UnsupportedOperationException(); 3 }
因为AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,所以AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了。例如公平锁的tryAcquire()在ReentrantLock.java的FairSync类中实现。
这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口,尽量减少不必要的工作量。
我们可以看看 ReentrantLock 在公平锁状态下是如何实现这个方法的,公平锁的tryAcquire()在ReentrantLock.java的FairSync类中实现,源码如下:
1 复制代码 2 protected final boolean tryAcquire(int acquires) { 3 // 获取“当前线程” 4 final Thread current = Thread.currentThread(); 5 // 获取“独占锁”的状态 6 int c = getState(); 7 // c=0意味着“锁没有被任何线程锁拥有”, 8 if (c == 0) { 9 // 若“锁没有被任何线程锁拥有”, 10 // 则判断“当前线程”是不是CLH队列中的排在最前面的那个线程, 11 // 若是的话,则获取该锁,设置锁的状态,并切设置锁的拥有者为“当前线程”。 12 if (!hasQueuedPredecessors() && 13 compareAndSetState(0, acquires)) { 14 setExclusiveOwnerThread(current); 15 return true; 16 } 17 } 18 else if (current == getExclusiveOwnerThread()) { 19 // 如果“独占锁”的拥有者已经为“当前线程”, 20 // 则将更新锁的状态,重入数加一 21 int nextc = c + acquires; 22 if (nextc < 0) 23 throw new Error("Maximum lock count exceeded"); 24 setState(nextc); // 更新当前线程的重入数 25 return true; 26 } 27 return false; 28 }
说明:根据代码,我们可以分析出,tryAcquire()的作用就是尝试去获取锁。注意,这里只是尝试!尝试成功的话,返回true;尝试失败的话,返回false,后续再通过其它办法来获取该锁。
2. addWaiter(Node.EXCLUSIVE)
在介绍此方法前我们需要对 Node 这个类进行介绍。
Node就是CLH队列的节点。Node在AQS中实现,它的数据结构如下:
1 private transient volatile Node head; // CLH队列的队首 2 private transient volatile Node tail; // CLH队列的队尾 3 4 // CLH队列的节点 5 static final class Node { 6 static final Node SHARED = new Node(); 7 static final Node EXCLUSIVE = null; 8 9 // 线程已被取消,对应的waitStatus的值 10 static final int CANCELLED = 1; 11 // “当前线程的后继线程需要被unpark(唤醒)”,对应的waitStatus的值。 12 // 一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。 13 static final int SIGNAL = -1; 14 // 线程(处在Condition休眠状态)在等待Condition唤醒,对应的waitStatus的值 15 static final int CONDITION = -2; 16 // (共享锁)其它线程获取到“共享锁”,对应的waitStatus的值 17 static final int PROPAGATE = -3; 18 19 // waitStatus为“CANCELLED, SIGNAL, CONDITION, PROPAGATE”时分别表示不同状态, 20 // 若waitStatus=0,则意味着当前线程不属于上面的任何一种状态。 21 volatile int waitStatus; 22 23 // 前一节点 24 volatile Node prev; 25 26 // 后一节点 27 volatile Node next; 28 29 // 节点所对应的线程 30 volatile Thread thread; 31 32 // nextWaiter是“区别当前CLH队列是 ‘独占锁’队列 还是 ‘共享锁’队列 的标记” 33 // 若nextWaiter=SHARED,则CLH队列是“独占锁”队列; 34 // 若nextWaiter=EXCLUSIVE,(即nextWaiter=null),则CLH队列是“共享锁”队列。 35 Node nextWaiter; 36 37 // “共享锁”则返回true,“独占锁”则返回false。 38 final boolean isShared() { 39 return nextWaiter == SHARED; 40 } 41 42 // 返回前一节点 43 final Node predecessor() throws NullPointerException { 44 Node p = prev; 45 if (p == null) 46 throw new NullPointerException(); 47 else 48 return p; 49 } 50 51 Node() { // Used to establish initial head or SHARED marker 52 } 53 54 // 构造函数。thread是节点所对应的线程,mode是用来表示thread的锁是“独占锁”还是“共享锁”。 55 Node(Thread thread, Node mode) { // Used by addWaiter 56 this.nextWaiter = mode; 57 this.thread = thread; 58 } 59 60 // 构造函数。thread是节点所对应的线程,waitStatus是线程的等待状态。 61 Node(Thread thread, int waitStatus) { // Used by Condition 62 this.waitStatus = waitStatus; 63 this.thread = thread; 64 } 65 }
说明:
Node是CLH队列的节点,代表“等待锁的线程队列”。
1. 每个Node都会一个线程对应。
2. 每个Node会通过prev和next分别指向上一个节点和下一个节点,这分别代表上一个等待线程和下一个等待线程。
3. Node通过waitStatus保存线程的等待状态。
4. Node通过nextWaiter来区分线程是“独占锁”线程还是“共享锁”线程。如果是“独占锁”线程,则nextWaiter的值为EXCLUSIVE;如果是“共享锁”线程,则nextWaiter的值是SHARED。
addWaiter(Node.EXCLUSIVE)的作用是,创建“当前线程”的Node节点,且Node中记录“当前线程”对应的锁是“独占锁”类型,并且将该节点添加到CLH队列的末尾。
1 private Node addWaiter(Node mode) { 2 //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享) 3 Node node = new Node(Thread.currentThread(), mode); 4 5 //尝试快速方式直接放到队尾。 6 Node pred = tail; 7 if (pred != null) { 8 node.prev = pred; 9 if (compareAndSetTail(pred, node)) { 10 pred.next = node; 11 return node; 12 } 13 } 14 15 //上一步失败则通过enq入队。 16 enq(node); 17 return node; 18 }
说明:对于“公平锁”而言,addWaiter(Node.EXCLUSIVE)会首先创建一个Node节点,节点的类型是“独占锁”(Node.EXCLUSIVE)类型。然后,再将该节点添加到CLH队列的末尾。
其中 compareAndSetTail() 和 enq() 方法源码如下:
1 private final boolean compareAndSetTail(Node expect, Node update) { 2 return unsafe.compareAndSwapObject(this, tailOffset, expect, update); 3 } 4 5 private Node enq(final Node node) { 6 //CAS"自旋",直到成功加入队尾 7 for (;;) { 8 Node t = tail; 9 if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。 10 if (compareAndSetHead(new Node())) 11 tail = head; 12 } else {//正常流程,放入队尾 13 node.prev = t; 14 if (compareAndSetTail(t, node)) { 15 t.next = node; 16 return t; 17 } 18 } 19 } 20 }
说明:
1. compareAndSetTail也属于CAS函数,也是通过“本地方法”实现的。compareAndSetTail(expect, update)会以原子的方式进行操作,它的作用是判断CLH队列的队尾是不是为expect,是的话,就将队尾设为update。
2. enq()的作用很简单。如果CLH队列为空,则新建一个CLH表头;然后将node添加到CLH末尾。否则,直接将node添加到CLH末尾。
所以总的来说 addWaiter() 方法的作用,就是将当前线程添加到CLH队列中。这就意味着将当前线程添加到等待获取“锁”的等待线程队列中了。
3. acquireQueued(Node, int)
前面我们已经尝试获取锁失败。将当前线程添加到CLH队列中了。而acquireQueued()的作用就是逐步的去执行CLH队列的线程,如果当前线程获取到了锁,则返回;否则,当前线程进行休眠,直到唤醒并重新获取锁了才返回。下面,我们看看acquireQueued()的具体流程。
1 final boolean acquireQueued(final Node node, int arg) { 2 boolean failed = true;//标记是否成功拿到资源 3 try { 4 boolean interrupted = false;//标记等待过程中是否被中断过 5 6 for (;;) { 7 final Node p = node.predecessor();//拿到前驱 8 //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。 9 if (p == head && tryAcquire(arg)) { 10 setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。 11 p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了! 12 failed = false; 13 return interrupted;//返回等待过程中是否被中断过 14 } 15 16 //如果自己可以休息了,就进入waiting状态,直到被unpark() 17 if (shouldParkAfterFailedAcquire(p, node) && 18 parkAndCheckInterrupt()) 19 interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true 20 } 21 } finally { 22 if (failed) 23 cancelAcquire(node); 24 } 25 }
前面部分代码注释写的很清楚了,这里就不多加描述,不过我们需要注意一下这个条件判断的写法 if (p == head && tryAcquire(arg)),这里涉及到锁的公平性问题,“当前线程”会根据公平性原则进行阻塞等待,直到获取锁为止;并且返回当前线程在等待过程中有没有被中断过。后面文章会单独说明。
看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。
1 // 返回“当前线程是否应该阻塞” 2 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 3 // 前继节点的状态 4 int ws = pred.waitStatus; 5 // 如果前继节点是SIGNAL状态,则意味这当前线程需要被阻塞。此时,返回true。 6 if (ws == Node.SIGNAL) 7 return true; 8 // 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边 9 if (ws > 0) { 10 do { 11 node.prev = pred = pred.prev; 12 } while (pred.waitStatus > 0); 13 pred.next = node; 14 } else { 15 // 如果前继节点为“0”或者“共享锁”状态,则设置前继节点为SIGNAL状态。 16 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 17 } 18 return false; 19 }
说明:
(01) 关于waitStatus请参考下表(中扩号内为waitStatus的值),更多关于waitStatus的内容,可以参考前面的Node类的介绍。
CANCELLED[1] -- 当前线程已被取消 SIGNAL[-1] -- “当前线程的后继线程需要被unpark(唤醒)”。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。 CONDITION[-2] -- 当前线程(处在Condition休眠状态)在等待Condition唤醒 PROPAGATE[-3] -- (共享锁)其它线程获取到“共享锁” [0] -- 当前线程不属于上面的任何一种状态。
(02) shouldParkAfterFailedAcquire()通过以下规则,判断“当前线程”是否需要被阻塞。
规则1:如果前继节点状态为SIGNAL,表明当前节点需要被unpark(唤醒),此时则返回true。 规则2:如果前继节点状态为CANCELLED(ws>0),说明前继节点已经被取消,则通过先前回溯找到一个有效(非CANCELLED状态)的节点,并返回false。 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,并返回false。
如果“规则1”发生,即“前继节点是SIGNAL”状态,则意味着“当前线程”需要被阻塞。接下来会调用parkAndCheckInterrupt()阻塞当前线程,直到当前线程被唤醒才从parkAndCheckInterrupt()中返回。
parkAndCheckInterrupt()代码如下:
private final boolean parkAndCheckInterrupt() { // 通过LockSupport的park()阻塞“当前线程”。 LockSupport.park(this); // 返回线程的中断状态。 return Thread.interrupted(); }
说明:parkAndCheckInterrupt()的作用是阻塞当前线程,并且返回“线程被唤醒之后”的中断状态。
它会先通过LockSupport.park()阻塞“当前线程”,然后通过Thread.interrupted()返回线程的中断状态。
这里介绍一下线程被阻塞之后如何唤醒。一般有2种情况:
第1种情况:unpark()唤醒。“前继节点对应的线程”使用完锁之后,通过unpark()方式唤醒当前线程。
第2种情况:中断唤醒。其它线程通过interrupt()中断当前线程。
补充:LockSupport()中的park(),unpark()的作用 和 Object中的wait(),notify()作用类似,是阻塞/唤醒。
它们的用法不同,park(),unpark()是轻量级的,而wait(),notify()是必须先通过Synchronized获取同步锁。
关于LockSupport,我们会在之后的章节再专门进行介绍!
这里提个问题:
如果锁被占用,线程阻塞,如果调用阻塞线程的 interrupt() 方法,会取消获取锁吗?答案是否定的。
首先要知道 LockSupport.park() 会响应中断,但不会抛出 InterruptedException,并且Thread.interrupted()返回线程的中断状态时会清空中断状态,当前线程会因为自旋再次进入阻塞状态。
如果我们调用 lockInterruptibly() 方法,这个方法必须是当前锁未发生中断的情况下才能获取锁,它的实现原理是,如果
shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() 判断条件成立的话,不返回Thread.interrupted(),而是直接抛出InterruptedException,同时在 finally 语句中将CLH队列中代表当前线程的节点状态设置为 Node.CANCELLED
4. selfInterrupt()
最后我们来看看 selfInterrupted() ,为什么要自己产生一个中断,其源码如下:
private static void selfInterrupt() { Thread.currentThread().interrupt(); }
这必须结合acquireQueued()进行分析。如果在acquireQueued()中,当前线程被中断过,则执行selfInterrupt();否则不会执行。
在acquireQueued()中,即使是线程在阻塞状态被中断唤醒而获取到cpu执行权利;但是,它需要重新在循环中判断,如果该线程的前面还有其它等待锁的线程,根据公平性原则,该线程依然无法获取到锁。它会再次阻塞,直到该线程被它的前面等待锁的线程锁唤醒;线程才会获取锁,然后“真正执行起来”!
也就是说,在该线程“成功获取锁并真正执行起来”之前,它的中断会被忽略并且中断标记会被清除! 因为在parkAndCheckInterrupt()中,我们线程的中断状态时调用了Thread.interrupted()。该函数不同于Thread的isInterrupted()函数,isInterrupted()仅仅返回中断状态,而interrupted()在返回当前中断状态之后,还会清除中断状态。 正因为之前的中断状态被清除了,所以这里需要调用selfInterrupt()重新产生一个中断,记录线程的中断状态。
小结
好了,我们再重新捋一遍整个获取锁的过程
1 public final void acquire(int arg) { 2 if (!tryAcquire(arg) && 3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 4 selfInterrupt(); 5 }
1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
3. acquireQueued()使线程在等待队列中等待,有机会时(按公平性原则,轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
整个流程可以用下图来表示
release(int)
看完如何获取到锁之后,我们再来看一下如何释放这个独占锁。
以 ReentrantLock 为例,它释放锁的操作是 unlock(),当你看它底层具体实现时可以发现,其实他调用的正是 AQS 的 release(int) 方法。
此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。下面是release()的源码:
1 public final boolean release(int arg) { 2 if (tryRelease(arg)) { 3 Node h = head;//找到头结点 4 if (h != null && h.waitStatus != 0) 5 unparkSuccessor(h);//唤醒等待队列里的下一个线程 6 return true; 7 } 8 return false; 9 }
说明:逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点。
1. tryRelease()
此方法尝试去释放指定量的资源。下面是tryRelease()的源码:
1 protected boolean tryRelease(int arg) { 2 throw new UnsupportedOperationException(); 3 }
跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
同样,我们这里也看一下在公平锁策略下的 ReentrantLock 是如何实现这个方法的,tryRelease()在ReentrantLock.java的Sync类中实现,源码如下:
1 protected final boolean tryRelease(int releases) { 2 // c是本次释放锁之后的状态 3 int c = getState() - releases; 4 // 如果“当前线程”不是“锁的持有者”,则抛出异常! 5 if (Thread.currentThread() != getExclusiveOwnerThread()) 6 throw new IllegalMonitorStateException(); 7 8 boolean free = false; 9 // 如果“锁”已经被当前线程彻底释放即可重入数为0时,则设置“锁”的持有者为null,即锁是可获取状态。 10 if (c == 0) { 11 free = true; 12 setExclusiveOwnerThread(null); 13 } 14 // 如果当前重入数不为0表示当前是重入锁的一次释放,所以只更新可重入数为c,不将独占锁线程清空。 15 setState(c); 16 return free; 17 }
说明:
tryRelease()的作用是尝试释放锁。
1. 如果“当前线程”不是“锁的持有者”,则抛出异常。
2. 如果“当前线程”在本次释放锁操作之后,对锁的拥有状态是0(即,当前线程彻底释放该“锁”),则设置“锁”的持有者为null,即锁是可获取状态。同时,更新当前线程的锁的状态为0。
getExclusiveOwnerThread(), setExclusiveOwnerThread()在AQS的父类AbstractOwnableSynchronizer.java中定义,源码如下:
1 public abstract class AbstractOwnableSynchronizer 2 implements java.io.Serializable { 3 4 // “锁”的持有线程 5 private transient Thread exclusiveOwnerThread; 6 7 // 设置“锁的持有线程”为t 8 protected final void setExclusiveOwnerThread(Thread t) { 9 exclusiveOwnerThread = t; 10 } 11 12 // 获取“锁的持有线程” 13 protected final Thread getExclusiveOwnerThread() { 14 return exclusiveOwnerThread; 15 } 16 17 ... 18 }
2. unparkSuccessor()
在release()中“当前线程”释放锁成功的话,会唤醒当前线程的后继线程。
根据CLH队列的FIFO规则,“当前线程”(即已经获取锁的线程)肯定是head;如果CLH队列非空的话,则唤醒锁的下一个等待线程。
下面看看unparkSuccessor()的源码,它在AQS中实现。
1 private void unparkSuccessor(Node node) { 2 // 获取当前线程的状态 3 int ws = node.waitStatus; 4 // 如果状态<0,则设置状态=0 5 if (ws < 0) 6 compareAndSetWaitStatus(node, ws, 0); 7 8 // 获取当前节点的“有效的后继节点”,无效的话,则通过for循环进行获取。 9 // 这里的有效,是指“后继节点对应的线程状态<=0” 10 Node s = node.next; 11 if (s == null || s.waitStatus > 0) { 12 s = null; 13 for (Node t = tail; t != null && t != node; t = t.prev) 14 if (t.waitStatus <= 0) 15 s = t; 16 } 17 // 唤醒“后继节点对应的线程” 18 if (s != null) 19 LockSupport.unpark(s.thread); 20 }
这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个离头节点最近的一个等待位置。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次循环判断p==head就成立了),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了。
总结
“释放锁”的过程相对“获取锁”的过程比较简单。释放锁时,主要进行的操作,是更新当前线程对应的锁的状态。如果当前线程对锁已经彻底释放(即state=0),则设置“锁”的持有线程为null,设置当前线程的状态为空,然后唤醒后继线程。
acquireShared(int)
此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是acquireShared()的源码:
1 public final void acquireShared(int arg) { 2 if (tryAcquireShared(arg) < 0) 3 doAcquireShared(arg); 4 }
这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是:
- tryAcquireShared()尝试获取资源,成功则直接返回;
- 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。
这里就不举tryAcquireShared()的实现了,因为比较复杂,在后面会专门讲解这部分源码
1. doAcquireShared(int)
此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。下面是doAcquireShared()的源码:
1 private void doAcquireShared(int arg) { 2 final Node node = addWaiter(Node.SHARED);//加入队列尾部 3 boolean failed = true;//是否成功标志 4 try { 5 boolean interrupted = false;//等待过程中是否被中断过的标志 6 for (;;) { 7 final Node p = node.predecessor();//前驱 8 if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的 9 int r = tryAcquireShared(arg);//尝试获取资源 10 if (r >= 0) {//成功 11 setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程 12 p.next = null; // help GC 13 if (interrupted)//如果等待过程中被打断过,此时将中断补上。 14 selfInterrupt(); 15 failed = false; 16 return; 17 } 18 } 19 20 //判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt() 21 if (shouldParkAfterFailedAcquire(p, node) && 22 parkAndCheckInterrupt()) 23 interrupted = true; 24 } 25 } finally { 26 if (failed) 27 cancelAcquire(node); 28 } 29 }
重点分析一下获取锁后的操作:setHeadAndPropagate
1 private void setHeadAndPropagate(Node node, int propagate) { 2 Node h = head; // Record old head for check below 3 setHead(node); // head 指向自己 4 5 /** 6 * 如果读锁(共享锁)获取成功,或头部节点为空,或头节点取消,或刚获取读锁的线程的下一个节点为空, 7 * 或在节点的下个节点也在申请读锁,则在CLH队列中传播下去唤醒线程,怎么理解这个传播呢, 8 * 就是只要获取成功到读锁,那就要传播到下一个节点(如果一下个节点继续是读锁的申请, 9 * 只要成功获取,就再下一个节点,直到队列尾部或为写锁的申请,停止传播)。具体请看doReleaseShared方法。 10 * 11 */ 12 if (propagate > 0 || h == null || h.waitStatus < 0) { 13 Node s = node.next; 14 if (s == null || s.isShared()) 15 doReleaseShared(); 16 } 17 } 18 19 private void setHead(Node node) { 20 head = node; 21 node.thread = null; 22 node.prev = null; 23 }
小结
acquireShared()也就结束了,让我们再梳理一下它的流程:
1. tryAcquireShared()尝试获取资源,成功则直接返回;
2. 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作,这体现了共享锁的定义。
releaseShared()
看了前面获取所得过程,现在来看一下释放锁的过程。在AQS中是通过releaseShared()来释放共享锁的。此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。下面是releaseShared()的源码:
1 public final boolean releaseShared(int arg) { 2 if (tryReleaseShared(arg)) {//尝试释放资源 3 doReleaseShared();//唤醒后继结点 4 return true; 5 } 6 return false; 7 }
此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于可重入的考量;而共享模式下的releaseShared()则没有这种要求,一是共享的实质–多线程可并发执行;二是共享模式基本也不会重入吧(至少我还没见过),所以自定义同步器可以根据需要决定返回值。
1.doReleaseShared()
此方法主要用于唤醒后继。下面是它的源码:
1 private void doReleaseShared() { 2 for (;;) { 3 Node h = head; 4 if (h != null && h != tail) { // 从队列的头部开始遍历每一个节点 5 int ws = h.waitStatus; 6 // 如果节点状态为 Node.SIGNAL,将状态设置为0,设置成功,唤醒线程。 7 // 为什么会设置不成功,可能改节点被取消;还有一种情况就是有多个线程在运行该代码段,这就是PROPAGATE的含义吧。 8 // 如果一个节点的状态设置为 Node.SIGNAL,则说明它有后继节点,并且处于阻塞状态 9 if (ws == Node.SIGNAL) { 10 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 11 continue; // loop to recheck cases 12 unparkSuccessor(h); 13 } 14 // 如果状态为0,则设置为Node.PROPAGATE,设置为传播,该值然后会在什么时候变化呢? 15 // 在判断该节点的下一个节点是否需要阻塞时,会判断,如果状态不是Node.SIGNAL或取消状态, 16 // 为了保险起见,会将前置节点状态设置为Node.SIGNAL,然后再次判断,是否需要阻塞。 17 else if (ws == 0 && 18 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 19 continue; // loop on failed CAS 20 } 21 /** 22 * 如果处理过一次 unparkSuccessor 方法后,头节点没有发生变化,就退出该方法,那head在什么时候会改变呢? 23 * 当然在是抢占锁成功的时候,head节点代表获取锁的节点。一旦获取锁成功,则又会进入setHeadAndPropagate方法, 24 * 当然又会触发doReleaseShared方法,传播特性应该就是表现在这里吧。再想一下,同一时间,可以有多个多线程占有锁, 25 * 那在锁释放时,写锁的释放比较简单,就是从头部节点下的第一个非取消节点,唤醒线程即可, 26 * 为了在释放读锁的上下文环境中获取代表读锁的线程,将信息存入在 readHolds ThreadLocal变量中。 27 */ 28 if (h == head) // loop if head changed 29 break; 30 } 31 }
参考资料: