一、前言
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现
二、AbstractQueuedSynchronizer
AbstractQueuedSynchronizer (AQS)类如其名,抽象的队列式同步容器,AQS定义类一套多线程访问共享资源的同步器,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。
AQS以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现满足自身功能特性的需求。除此之外,AQS通过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节。
2.1 AQS队列节点数据结构
- prev:指向队列中的上一个节点;
- waitStatus:节点的等待状态,初始化为0,表示正常同步等待:
- CANCELLED=1:节点因超时或者被中断而取消时设置为取消状态;
- SIGNAL=-1:指示当前节点被释放后,需要调用unpark通知后面节点,如果后面节点发生竞争导致获取锁失败,也会将当前节点设置为SIGNAL;
- CONDITION=-2:指示该线程正在进行条件等待,条件队列中会用到;
- PROPAGATE=-3:共享模式下释放节点时设置的状态,表示无限传播下去。
- thread:当前节点操作的线程;
- nextWaiter:该字段在Condition条件等待中会用到,指向条件队列的下一个节点。或者链接到SHARED常量,表示节点正在以共享模式等待;
- next:指向队列中的下一个节点。
2.2 AQS同步器数据结构
AQS中同步等待队列的实现是一个带头尾指针(这里用指针表示引用是为了后面讲解源码时可以更直观形象,况且引用本身是一种受限的指针)且不带哨兵结点(后文中的头结点表示队列首元素结点,不是指哨兵结点)的双向链表。
如上图,AQS中:
- state:所有线程通过通过CAS尝试给state设值,当state>0时表示被线程占用;同一个线程多次获取state,会叠加state的值,从而实现了可重入;
- exclusiveOwnerThread:在独占模式下该属性会用到,当线程尝试以独占模式成功给state设值,该线程会把自己设置到exclusiveOwnerThread变量中,表明当前的state被当前线程独占了;
- 等待队列(同步队列):等待队列中存放了所有争夺state失败的线程,是一个双向链表结构。state被某一个线程占用之后,其他线程会进入等待队列;一旦state被释放(state=0),则释放state的线程会唤醒等待队列中的线程继续尝试cas设值state;
- head:指向等待队列的头节点,延迟初始化,除了初始化之外,只能通过setHead方法进行修改;
- tail:指向等待队列的队尾,延迟初始化,只能通过enq方法修改tail,该方法主要是往队列后面添加等待节点。
为了接下来能够更好的理解加锁和解锁过程的源码,对该同步队列的特性进行简单的讲解:
- 1、同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己。如何才能线程安全的实现入队是后面讲解的重点,毕竟我们在讲锁的实现,这部分代码肯定是不能用锁的。
- 2、队列首结点可以用来表示当前正获取锁的线程。
- 3、当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程。
同步状态变量
private volatile int state; //同步状态变量这是一个带volatile前缀的int值,是一个类似计数器的东西。在不同的同步组件中有不同的含义。以ReentrantLock为例,state可以用来表示该锁被线程重入的次数。当state为0表示该锁不被任何线程持有;当state为1表示线程恰好持有该锁1次(未重入);当state大于1则表示锁被线程重入state次。因为这是一个会被并发访问的量,为了防止出现可见性问题要用volatile进行修饰。
持有同步状态的线程标志
/** * The current owner of exclusive mode synchronization. */ //持有同步状态的线程标志 private transient Thread exclusiveOwnerThread;如注释所言,这是在独占同步模式下标记持有同步状态线程的。ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪个线程持有。
AbstractQueuedSynchronizer自定义同步器实现
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了,自定义同步容器实现时主要实现以下几种方法:
- 1、isHeldExclusively():该线程是否正在独占资源,只有用到condition才需要取实现它。
- 2、tryAcquire(int):独占方式,尝试获取资源,成功则返回true,失败则返回false。
- 3、tryRelease(int):独占方式,尝试释放资源,成功则返回true,失败则返回false。
- 4、tryAcquireShared(int):共享方式,尝试获取资源,附属表示获取失败,0表示获取成功,但是没有剩余资源可用,其他线程不能在获取,正数表示获取成功,并且有剩余资源,也就是自己可以重复获取(可重入)。
- 5、tryReleaseShare(int):共享方式。尝试释放资源,成功返回true,失败返回false。
- 6、以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()的时候,会调用tryAcquire()独占该锁并将state+1。此后其他线程在tryAcquire()独占该锁并将state+1。此后表示其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(几锁释放)为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
了解AQS的主要结构后,就可以开始进行ReentrantLock的源码解读了。
三、ReentrantLock
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
-
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取。
-
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
-
可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
-
可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。
3.1 Reentrantlock类结构
public class ReentrantLock implements Lock, java.io.Serializable { }可以看出Reentrantlock 实现类lock接口,首先看下这个lock接口。
public interface Lock { //普通的获取锁的方法,lock会阻塞直到成功 void lock(); //与lock不同的时,它可以相应中断, void lockInterruptibly() throws InterruptedException; //尝试获取锁,立即返回,不阻塞,成功返回true,失败返回false boolean tryLock(); //先尝试获取锁,如果成功则返回true,失败则阻塞等待,等待时间由参数决定, //在等待的同时相应中断,抛出中断异常,如果在等待的过程中获得类锁则返回true, //否则直到时间超时,则返回false。 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //普通释放锁的方法 void unlock(); //新建一个条件,lock可以关联多个条件,关于条件在以后篇幅中记录 Condition newCondition(); }可以看出,显示锁,与synchronied相比,支持非阻塞的方式获取锁,可以响应中断,可以限时,这使得它非常的灵活。
现在回到Reentrantlock类,可以看到其内部基本上定义类三个内部类:
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; abstract static class Sync extends AbstractQueuedSynchronizer { //...... } //非公平锁 static final class NonfairSync extends Sync { //...... } //公平锁 static final class FairSync extends Sync { //...... } }很显然从上面的类结构,我们可以得出jdk利用CAS算法和AQS实现类ReentrantLock。
3.2 构造函数分析
- ReentrantLock出于性能考虑,默认采用非公平锁。
四、NonfairSync非公平锁
NonfairSync非公平锁加锁流程(简化):
- 1、基于CAS尝试将state(锁数量)从0设置为1。
- A、如果设置成功,设置当前线程为独占锁的线程;
- B、如果设置失败,还会再获取一次锁数量,
-
B1、如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
-
B2、如果锁数量不为0或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。
-
4.1 非公平模式下的加锁
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; static final class NonfairSync extends Sync { final void lock() { //直接通过CAS,通过改变共享资源 state值,成功则设置当前线程为持有锁的线程(独占), //失败则调用顶层AQS的acquire方法,再次尝试,失败则将线程放到阻塞队列 if (compareAndSetState(0, 1)) //将当前线程标记为已持有锁 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } } }ReentrantLock 中使用 state 用于表示是否被锁和持有的数量,如果当前未被锁定,则立即获得锁,否则调用acquire(1);方法来获得锁,其中acquire为AQS中的方法,其实现为:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * 该方法主要做了三件事情: * 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回 * 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点, * 并将节点放入同步队列尾部 * 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁 */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } }4.1.1、tryAcquire(arg)尝试去获取锁。如果尝试获取锁成功,方法直接返回。
- 这段代码调用 tryAcquire 方法来尝试获取锁,该方法需要被子类重写,在 NonFairSync 中的实现为:
非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。
不同的锁可以有不同的tryAcquire()实现,所以,你可以看到ReentrantLock锁里面会有非公平锁和公平锁的实现方式。 ReentrantLock公平锁的实现代码在获取锁之前多了一个判断:!hasQueuedPredecessors(),这个是判断如果当前线程节点之前没有其他节点了,那么我们才可以尝试获取锁,这就是公平锁的体现。
4.1.2、进入等待队列。由于上文中提到锁已经被占用了,所以执行tryAcquire失败,并且入等待队列。如果锁一直不释放,那么后续线程就会被挂起。
- 先看下入队的过程。先看addWaiter(Node.EXCLUSIVE)
- 假如多个线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设同时走到了enq(node)里。
这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。
当线程进入等待队列后,此时AQS队列如下:
4.1.3、挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * 已经入队的线程尝试获取锁 */ final boolean acquireQueued(final Node node, int arg) { //标记是否成功获取锁 boolean failed = true; try { //标记线程是否被中断过 boolean interrupted = false; for (;;) { //获取前驱节点 final Node p = node.predecessor(); //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁 if (p == head && tryAcquire(arg)) { // 获取成功,将当前节点设置为head节点 setHead(node); // 原head节点出队,在某个时间点被GC回收 p.next = null; // help GC //获取成功 failed = false; //返回是否被中断过 return interrupted; } // 判断获取失败后是否可以挂起,若可以则挂起 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 线程若被中断,设置interrupted为true interrupted = true; } } finally { if (failed) cancelAcquire(node); } } }code里的注释已经很清晰的说明了acquireQueued的执行流程。假设B和C在竞争锁的过程中A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第2个if语句中。
我们再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * 判断当前线程获取锁失败之后是否需要挂起. */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //前驱节点的状态 int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 前驱节点状态为signal,返回true return true; // 前驱节点状态为CANCELLED if (ws > 0) { // 从队尾向前寻找第一个状态不为CANCELLED的节点 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 将前驱节点的状态设置为SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } /** * 挂起当前线程,返回线程中断状态并重置 */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } }线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。
注意: 是否需要 unpark 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定,而不是本节点的waitStatus 决定。
如果当前节点的prev不是head,或者争抢失败,则会将前面节点的状态设置为SIGNAL。
如果前面的节点状态大于0,表示节点被取消,这个时候会把该节点从队列中移除掉。
下图为尝试CAS争抢锁,但失败了,然后把head节点状态设置为SIGNAL
然后再会循环一次尝试获取锁,如果获取失败了,就调用LockSupport.park(this)挂起线程。
那么时候才会触发唤起线程呢?这个时候我们得先看看释放锁是怎么做的了。
五、FairSync公平锁
FairSync公平锁加锁流程(简化):
-
1、如果锁数量为0,如果当前线程是等待队列中的头节点,基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
-
2、如果锁数量不为0或者当前线程不是等待队列中的头节点或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。
5.1 公平模式下的加锁
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; static final class FairSync extends Sync { final void lock() { acquire(1); } } }调用acquire(1);方法来获得锁,其中acquire为AQS中的方法,其实现为:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * 该方法主要做了三件事情: * 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回 * 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点, * 并将节点放入同步队列尾部 * 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁 */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } }5.2 FairSync:tryAcquire(int acquires)
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; static final class FairSync extends Sync { protected final boolean tryAcquire(int acquires) { //获取当前线程 final Thread current = Thread.currentThread(); //获取state变量值 int c = getState(); if (c == 0) { //如果没有线程持有锁 //调用hasQueuedPredecessors函数判断队列中是否有优先于此线程的排队线程 //如果没有才去尝试竞争锁 // hasQueuedPredecessors()和非公平锁相比,这里多了一个判断:是否有线程在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //占用锁成功,设置独占线程为当前线程 setExclusiveOwnerThread(current); return true; } } //当前线程已经占用该锁 锁重入 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); // 更新state值为新的重入次数 setState(nextc); return true; } //获取锁失败 return false; } } }可以看到这里的逻辑和NonfairSync唯一的区别就是FairSync多了hasQueuedPredecessors函数的调用,在竞争锁之前需要先看队列中是否有优先于此线程的排队线程,如果有的话,那么FairSync不会去竞争锁,而是直接进入后面入队阻塞的逻辑。那么这里强调的优先于此线程的排队线程是什么意思呢?因为FairSync是公平锁的实现,获取锁总要有个先来后到。
5.3 hasQueuedPredecessors
- 判断队列中是否有优先于此线程的排队线程
步骤:
- 1、如果h==t成立,h和t均为null或是同一个具体的节点,无后继节点,返回false。
- 2、如果h!=t成立,head.next是否为null,如果为null,返回true。
- 什么情况下h!=t的同时h.next==null?,有其他线程第一次正在入队时,可能会出现。见AQS的enq方法,compareAndSetHead(node)完成,还没执行tail=head语句时,此时tail=null,head=newNode,head.next=null。
- 3、如果h!=t成立,head.next != null,则判断head.next是否是当前线程,如果是返回false,否则返回true(head节点是获取到锁的节点,但是任意时刻head节点可能占用着锁,也可能释放了锁(unlock()),未被阻塞的head.next节点对应的线程在任意时刻都是有必要去尝试获取锁)
六、释放锁
NonfairSync非公平锁和FairSync公平锁的释放锁代码一样。
6.1 unlock()
步骤:
-
1、获取当前的锁数量,然后用这个锁数量减去解锁的数量(这里为1),最后得出结果c。
-
2、判断当前线程是不是独占锁的线程,如果不是,抛出异常。
-
3、如果c==0,说明锁被成功释放,将当前的独占线程置为null,锁数量置为0,返回true。
-
4、如果c!=0,说明释放锁失败,锁数量置为c,返回false。
-
5、如果锁被释放成功的话,唤醒距离头节点最近的一个非取消的节点。
6.2 ReentrantLock:unlock()
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; /** * 释放这个锁 *1)如果当前线程持有这个锁,则锁数量被递减 *2)如果递减之后锁数量为0,则锁被释放。 *如果当前线程不持久有这个锁,抛出异常 */ public void unlock() { sync.release(1); } } public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { public final boolean release(int arg) { //如果成功释放锁 if (tryRelease(arg)) { //获取头节点:(注意:这里的头节点就是当前正在释放锁的节点) Node h = head; //头结点存在且等待状态不是取消 if (h != null && h.waitStatus != 0) //唤醒距离头节点最近的一个非取消的节点 unparkSuccessor(h); return true; } return false; } }6.3 Sync:tryRelease(int releases)
最后我们再看下tryRelease的执行过程
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; /** * 释放当前线程占用的锁 * @param releases * @return 是否释放成功 */ abstract static class Sync extends AbstractQueuedSynchronizer { protected final boolean tryRelease(int releases) { // 计算释放后state值 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果不是当前线程占用锁,那么抛出异常 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 锁被重入次数为0,表示释放成功 free = true; // 清空独占线程 setExclusiveOwnerThread(null); } // 更新state值 setState(c); return free; } } }6.4 AbstractQueuedSynchronizer:unparkSuccessor(Node node)
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * 唤醒离头节点node最近的一个非取消的节点 * @param node 头节点 */ private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) //将ws设为0状态(即什么状态都不是) compareAndSetWaitStatus(node, ws, 0); /* * 获取头节点的下一个等待状态不是cancel的节点 */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; /* * 注意:从后往前遍历找到离头节点最近的一个非取消的节点, * 从后往前遍历据说是在入队(enq())的时候,可能nodeX.next==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()具体由子类实现。一般处理流程是让state减1。
如果释放锁成功,并且头节点waitStatus!=0,那么会调用unparkSuccessor()通知唤醒后续的线程节点进行处理。
注意:在遍历队列查找唤醒下一个节点的过程中,如果发现下一个节点状态是CANCELLED那么就会忽略这个节点,然后从队列尾部向前遍历,找到与头结点最近的没有被取消的节点进行唤醒操作。
唤醒之后,节点对应的线程2又从acquireQueued()方法的阻塞处醒来继续参与争抢锁。并且争抢成功了,那么会把head节点的下一个节点设置为null,让自己所处的节点变为head节点:
这样一个AQS独占式、非中断的抢占锁的流程就结束了。
七、完整流程
最后我们再以另一个维度的流程来演示下这个过程。
首先有4个线程争抢锁,线程1,成功了,其他三个失败了,分别依次入等待队列:
线程2、线程3依次入队列:
现在突然发生了点事情,假设线程3用的是带有超时时间的tryLock,超过了等待时间,线程3状态变为取消状态了,这个时候,线程4追加到等待队列中后,发现前一个节点的状态是1取消状态,那么会执行操作把线程3节点从队列中移除掉:
最后,线程1释放了锁,然后把head节点ws设置为0,并且找到了离head最靠近的一个waitStatus<=0的线程并唤醒,然后参与竞争获取锁:
最终,线程2获取到了锁,然后把自己变为了Head节点,并取代了原来的Head节点:
接着就一直这样循环,我就不再画图了,聪明的你应该对此了如指掌了。
八、可打断原理
8.1 不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { private final boolean parkAndCheckInterrupt() { // 如果打断标记已经是 true, 则 park 会失效 LockSupport.park(this); // interrupted 会清除打断标记 return Thread.interrupted(); } 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()) // 如果是因为 interrupt 被唤醒, 返回打断状态为 true interrupted = true; } } finally { if (failed) cancelAcquire(node); } } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果打断状态为 true selfInterrupt(); } static void selfInterrupt() { // 重新产生一次中断 Thread.currentThread().interrupt(); } }8.2 可打断模式
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // 如果没有获得到锁, 进入 ㈠ if (!tryAcquire(arg)) doAcquireInterruptibly(arg); } // ㈠ 可打断的获取锁流程 private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 在 park 过程中如果被 interrupt 会进入此 // 这时候抛出异常, 而不会再次进入 for (;;) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } }九、总结:
9.1 公平锁与非公平锁对比
- FairSync:lock()少了插队部分(即少了CAS尝试将state从0设为1,进而获得锁的过程)
- FairSync:tryAcquire(int acquires)多了需要判断当前线程是否在等待队列首部的逻辑(实际上就是少了再次插队的过程,但是CAS获取还是有的)。
ReentrantLock是基于AbstractQueuedSynchronizer实现的,AbstractQueuedSynchronizer可以实现独占锁也可以实现共享锁,ReentrantLock只是使用了其中的独占锁模式。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
参考: https://blog.csdn.net/fuyuwei2015/article/details/83719444
https://blog.51cto.com/u_3265857/2369641
https://segmentfault.com/a/1190000014769953
https://www.jb51.net/article/105762.htm
https://www.cnblogs.com/java-zhao/p/5133402.html
https://www.cnblogs.com/java-zhao/p/5131544.html
https://www.itzhai.com/articles/aqs-and-lock-implementation-in-concurrent-packages.html