目录
获取锁,调用该方法当前线程将会获取锁,当锁获取后,该方法将返回。该方法优先考虑获取锁,待获取锁成功后,才响应中断,在获取到锁之前,不会响应中断。
lock()方法涉及到非公平锁和公平锁两种不同的实现方法,这里我们就从公平锁和非公平锁两个角度来讲解源码的实现。
后面的其他获取锁的方法有一些也会因为RenntractLock属性sync的实现类不同,而导致实现源码不同的情况。也就是会因公平锁和非公平锁的不同而采用不同的实现方法,但是依旧是使用不同的sycn实现类方法而已。
1 公平锁
这里我们假设ReentrantLock的实例是通过以下方式获得的:
ReentrantLock reentrantLock = new ReentrantLock(true);
public ReentrantLock(booleanfair) {
sync = fair ? new FairSync() : new NonfairSync();
}
下面的是加锁的主要逻辑:
// ReentrantLock.lock()
public void lock() {
// 调用的sync属性的lock()方法
// 我们这一节讲公平锁,所以这里的sync是FairSync的实例
sync.lock();
}
// ReentrantLock.FairSync.lock()
final void lock() {
// 调用AQS的acquire()方法获取锁
// 注意,这里传的值为1
acquire(1);
}
// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) { // AQS提供的可以直接调用的模板方法
// 尝试获取锁
if (!tryAcquire(arg) && // 锁竞争逻辑
/**
* 如果上面获取锁失败了,就入队等待
* 1、addWaiter():为当前线程创建一个Node,添加到阻塞等待队列中,队列就是一个双向链表。在这个方法中的线程还是在运行当中的
* 注意addWaiter()这里传入的节点模式为独占模式。addWaiter()入参是用来标识当前锁是共享锁还是互斥锁的标志,ReentrantLock是一个互斥锁,所以这里传入的是Node.EXCLUSIVE互斥标志
* 2、acquireQueued():对新添加到队列中的等待线程进行阻塞,线程是在这个方法中被阻塞的。线程被阻塞后再被唤醒,会继续在这个方法中自旋去调用tryAcquire(arg)尝试获取锁,最后成功获取锁并返回
*/
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 这一步操作就是调用中断方法来改变线程的中断状态,这样做就可以把中断信号往外层转递,程序员自己设计的代码也就可以识别到中断信号来自定义相关响应操作。执行这个方法是当前线程肯定是没有获取到锁
selfInterrupt();
}
// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) { // tryAcquire方法(尝试非阻塞获取锁)是由AQS进行声明,但是需要使用者自行实现的方法。所以这个方法是由ReentrantLock实现的
// 当前线程
final Thread current = Thread.currentThread();
// 查看当前状态变量的值
int c = getState();
// 如果状态变量的值为0,说明暂时还没有人占有锁,可以去尝试获取锁
if (c == 0) {
// !hasQueuedPredecessors():如果没有其它线程在排队,那么当前线程尝试更新state的值为acquires
// 如果更新成功,则说明当前线程获取了锁
if (!hasQueuedPredecessors() && // 判断当前是否已经有其他线程正在等待。 通过这个方法就保证了如果等待队列中第一个线程节点被release()唤醒来抢占锁,它是100%能够成功获取锁的,这就是公平锁的特性。如果是非公平锁,当队列头部节点被release()唤醒后就不一定能抢占到锁了,有可能被别的线程抢先一步抢占
compareAndSetState(0, acquires)) { // 相当于一个比较交换的原子操作,由unsafe提供的native本地方法
// 当前线程获取了锁,把自己设置到exclusiveOwnerThread变量中
// exclusiveOwnerThread是AQS的父类AbstractOwnableSynchronizer中提供的变量
setExclusiveOwnerThread(current);
// 返回true说明成功获取了锁
return true;
}
}
// 如果当前线程本身就占有着锁,现在又尝试获取锁,那么,直接让它获取锁并返回true。这里我们就可以看出来ReentrantLock实现了可重入性
else if (current == getExclusiveOwnerThread()) {
// 状态变量state的值加acquires
int nextc = c + acquires;
// 如果溢出了,则报错
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 将累加的新状态量设置到state变量中
// 这里不需要CAS更新state
// 因为当前线程占有着锁,其它线程只会CAS把state从0更新成1,是不会成功的
// 所以不存在竞争,自然不需要使用CAS来更新
setState(nextc);
// 当线程获取锁成功
return true;
}
// 当前线程尝试获取锁失败
return false;
}
// AbstractQueuedSynchronizer.hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
Node t = tail;
// head指针节点
Node h = head;
Node s;
// 这个方法就是查看队头有没有线程节点,如果有就看一下这个队头线程节点是不是当前线程节点,如果队头存在节点并且不是当前线程节点,就说明此时队列中有别的线程正在等待,返回true
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
// AbstractQueuedSynchronizer.addWaiter()
// 调用这个方法,说明上面tryAcquire()尝试获取锁失败了。addWaiter方法传入的参数是Node的属性:共享属性(Node.SHARED),独占(互斥)属性(Node.EXCLUSIVE)
// 线程入队,将Node节点加入同步对联,而Node节点是与Thread绑定的,也就相当于将线程入队。addWaiter这个操作将线程入队之后,并没有将线程阻塞,对线程的阻塞操作在acquireQueued方法中
创建节点Node,Node中的重要属性有这些: pre,next,waitestate,thread
waitestate节点的生命状态:信号量 一个节点的waitestate属性值表示的是这个节点下一个节点的生命状态:
SIGNAL = -1 //可被唤醒 指这个节点的下一个节点可被唤醒
CANCELLED = 1 //代表出现异常,中断引起的,需要废弃结束
CONDITION = -2 // 条件等待,这个是在条件锁中使用的Node状态
PROPAGATE = -3 // 传播,可以用来广播的状态
0 // 初始状态Init状态。一个对象被创建出来,int类型的属性默认值都是0
private Node addWaiter(Node mode) {
// 为当前线程创建一个Node节点。Node是AbstractQueuedSynchronizer中的一个静态内部类,主要用来存储线程信息
Node node = new Node(Thread.currentThread(), mode);
/**
* 这里先尝试把新节点加到尾节点后面(尾插法)
* 如果成功了就返回新节点
* 如果没成功再调用enq()方法不断尝试添加
*/
// 获取尾节点
Node pred = tail;
// 如果尾节点不为空,则将新建节点尾插到CLH等待队列中
if (pred != null) {
// 设置新节点的前驱节点为现在的尾节点
node.prev = pred;
// CAS更新尾节点为新节点
if (compareAndSetTail(pred, node)) {
// 如果成功了,把旧尾节点的下一个节点指向新节点
pred.next = node;
// 并返回新节点
return node;
}
}
// 如果上面尝试入队新节点没成功(有两个原因:1、被别的线程抢先插入了节点。 2、当前CLH队列还未初始化),调用enq()处理
enq(node);
return node;
}
// AbstractQueuedSynchronizer.enq()
// 将线程节点添加到等待队列
private Node enq(final Node node) {
// 自旋,不断尝试将节点添加到CLH等待队列
for (;;) {
// 本轮循环获取现在最新的队列尾节点
Node t = tail;
/**
* 如果尾节点为空,说明队列还未初始化
* 这个分支是为了给CLH队列初始化,在AQS中规定,如果head=tail=null,就说明这个队列还没有初始化。
* 创建一个空的Node节点,然后要head和tail都先指向这个空的节点,这样就完成CLH队列初始化。
* 其实就相当于队列头指向的是一个空节点,然后空节点后面的才是真正存储信息的Node节点。
* 之所以这样设计,是因为每个节点的waitStatus = SIGNAL时,表示的是下一个节点的生存状态,所以队列中第一个存有线程的节点,必须也要有一个前驱结点来标识它的生存状态
*/
if (t == null) {
// 初始化head头指针节点和tail尾指针节点
if (compareAndSetHead(new Node())) // 创建一个空的Node节点,并且用CAS将其赋值给head属性
// 将尾节点指向当前初始化的头节点,完成队列初始化。然后在下一轮循环中就可以尝试将Node节点插入队列了
tail = head;
} else {
// 如果尾节点不为空,说明当前队列已经完成了初始化,可以尝试向队列中插入Node节点
// 设置新节点的前一个节点为现在的尾节点
node.prev = t;
// 因为入队也存在竞争,所以通过一个比较交换的原子操作,先看一下当前队列的尾节点是不是我们预想的尾节点t,如果是,说明队列还没有被修改过,插入不存在并发风险,直接插入。
// 如果不是t,说明队列已经被修改,需要重新继续进行自旋,获取最新的tail,来准备入队。
if (compareAndSetTail(t, node)) { // compareAndSetTail方法在判断可以将node尾插到尾部后,就会将node的pre节点指向现在的tail,然后再将tail指向刚刚插入的node节点,这两步是用原子操作完成的
// 成功了,则设置旧尾节点的下一个节点为新节点
t.next = node;
// 并返回旧尾节点,结束自旋
return t;
}
}
// 开始新一轮循环,更新最新的tail节点
}
}
// AbstractQueuedSynchronizer.acquireQueued()
// 调用上面的addWaiter()方法使得新节点已经成功入队了,下面这个方法就是将当前节点阻塞。当线程被唤醒之后,也会在这个方法中继续自旋调用tryAcquire(arg)尝试获取锁,直到成功获取锁后返回
// 该方法的返回值是中断标记,如果该方法是正常返回的,那么说明当前线程一定已经成功获取锁了,这个返回值只是用来判断该线程的中断标志的
// 如果是true,说明线程是被中断唤醒的,需要响应中断信号,如果是false,则说明线程没有被中断
/**
如果当前节点是队列中的第一个线程节点,节点阻塞之前还得再尝试一次获取锁:
1、能够获取到,节点出队,并且把head指针往后挪一个节点,也就是指向了当前节点,然后将当前节点的内部属性都清空,当前节点就变成了head指针节点,也就完成了当前节点的出队
2、不能获取到,将节点阻塞,然后等待被唤醒
1.首先第1轮循环,修改当前节点的前驱节点的的waitStatus生命状态,修改成sinal=-1,表示当前节点可以被唤醒,只有保证当前节点可以被唤醒之后,才可以放心的将其阻塞
2.第2轮循环,阻塞线程,并且需要判断线程是否是由中断信号唤醒的,进而设置中断信号,在该线程成功获取到锁之后去响应中断
*/
final boolean acquireQueued(final Node node, int arg) {
// 阻塞线程是否失败
boolean failed = true;
try {
// 中断标记,是否要将当前线程中断
boolean interrupted = false;
// 自旋
for (;;) {
// 获取当前节点的前一个节点
final Node p = node.predecessor();
// 判断当前节点的前一个节点是否为head指针节点,如果是,说明当前节点是队列中的第一个线程节点,则不对其进行阻塞,来让这个线程再去尝试抢一次资源。
// 因为阻塞和唤醒操作涉及到用户态和内核态的上下文切换,非常耗时,尽量避免阻塞和唤醒操作。不管是公平锁还是非公平锁acquireQueued()的操作都是一样的
if (p == head && tryAcquire(arg)) { // 调用ReentrantLock.FairSync.tryAcquire()方法再次尝试获取锁,如果是公平锁,头部节点被唤醒一定能成功获取到锁,但是非公平锁就不一定了
// 尝试获取锁成功,下面就需要将当前节点的node出队
// 将等待队列队中第一个线程节点node出队并不会涉及到并发异常的问题,因为仅仅只是将head指针向后移动一位,不涉及到其他的更改,所以不需要用CAS更新
// 将head指针节点指向node,并且将node节点的前驱节点和代表的线程属性都设置为null
setHead(node);
/**
* p是原head指针节点,也就是原来node节点的前驱结点,将原head指针节点从队列中删除
* 将p节点,也就是head指针节点的next设置为空。
* 此时head和tail都指向空的node节点,这里虽然没有操作tail的指向,但是在进入到acquireQueued之前,tail就指向了node节点(将node尾插入队列,tail就会指向node节点),
* 在调用setHead方法的时候,又让head重新指向了node,所以此时head和tail又指向了同一个空节点,恢复到了初始状态
*/
p.next = null; // 这个p.next指向的是原来head指向的节点,但是现在head向后移动了一个位置,所以p节点,也就是原head节点也没什么用了,将其从队列中删除,将它的next指向设置为空,p节点就成了不可达对象,会被GC回收
// 将阻塞标志设置为false,说明没有阻塞节点线程
failed = false;
/**
* 通过这个源码我们就发现,如果使用lock()方法获取锁,在尝试获取锁的过程中这个方法时忽略了中断信号的。
* 但是在尝试获取锁的过程中,当前线程的中断状态被一直记录在了该方法中的局部变量interrupted中
* 当该线程成功获取到锁的时候,才会响应中断信号,将记录在interrupted局部变量中的中断信号向上一层返回,在上一层的selfInterrupt()调用Thread.currentThread().interrupt()方法响应中断
* 所以lock()方法优先考虑获取锁,待获取锁成功后,才响应中断,在获取到锁之前,不会响应中断。
*/
// 返回中断标记,告诉上一层调用位置,当前线程要不要对其进行中断
return interrupted;
}
/**
* 如果上面没有成功获取到资源,就需要对当前的线程进行阻塞。
* 1、首先判断node节点能否被阻塞
* shouldParkAfterFailedAcquire()方法用来判断node节点能都被阻塞。
* 这里就是用到了每个Node节点的watStatus属性,只有当一个节点的前驱节点的watStatus属性是-1(可被唤醒)的时候,该节点才可以被从队列中唤醒。
* 这个方法就是用来判断当前node节点是不是可唤醒的状态,只有这个node节点是可唤醒的状态,我们才可以对其进行阻塞。
* 因为如果这个节点不能被唤醒的状态我们就对他阻塞,这个节点将永远不能被唤醒,就会留在线程栈空间中,造成空间浪费堆积。
*
* 2、然后对node节点进行阻塞
* parkAndCheckInterrupt()方法用来对node节点进行阻塞
* 阻塞的线程会停留在parkAndCheckInterrupt方法中,直到被唤醒。当被阻塞节点在队列中排到第一个节点时,调用release()就会将其唤醒
* 被唤醒之后就会继续进行循环自旋,在下一轮的循环中执行到if (p == head && tryAcquire(arg))分支时,被唤醒的队列头线程节点(队列头线程节点指的是head指针节点的下一个节点)就能够成功获取到资源了。
* 注意除了队列头线程节点被唤醒之外,其他后面的节点还是处在阻塞状态,不在自旋,这也就是我们前面所说的节约CPU资源。
*/
// 判断是否可以将node节点阻塞,如果可以则将其阻塞
if (shouldParkAfterFailedAcquire(p, node) && // 判断node节点能否被唤醒
// 真正对node节点进行阻塞的方法,阻塞之后这个线程就会停在这个方法内
parkAndCheckInterrupt())
// 如果parkAndCheckInterrupt()执行了阻塞并且返回当前线程的中断信号是true,则将这个方法中设置的中断信号变量设置为true,用来向上一层返回当前线程的中断标志
interrupted = true;
}
} finally {
/**
* failed标识当前线程的阻塞有没有失败
* acquireQueued()最初始把failed设置为true,只有进入到成功获取到锁的分支中,才会将failed设置为false。
* 所以除了成功获取锁return导致try{}执行结束以外,其他任何情况导致try{}执行结束都会使failed仍然等于true,也就是没有成功将线程阻塞过
*
* waitStatus = CANCELLED = 1,表示线程已被取消(等待超时或者被中断),需要废弃结束。
* 说的就是这种情况,当一个线程在acquireQueued()方法中因为被中断(响应中断的方法lockInterruptibly()在识别到终端信号之后会直接抛出异常就会将try{}代码块执行结束掉)或等待超时等原因而提前结束,就需要将该线程node节点生命状态设置为CANCELLED
* 在未来该节点的后继节点进入到shouldParkAfterFailedAcquire()方法时,就会将该节点给移出队列
*/
// 如果阻塞线程失败了
if (failed)
// 取消获取锁
cancelAcquire(node);
}
}
// AbstractQueuedSynchronizer.setHead()
/**
* 将head指向node节点,并且将node节点中的thread线程属性,prev前驱结点指向都清空。将node节点构造成一个空节点
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
// AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()
/**
* 这个方法是在上面的for()循环里面调用的。该方法用来判断node节点能不能被阻塞,判断规则就是看node节点是不是可被唤醒,只有可被唤醒的节点才可以放心将其阻塞。
* 通过获取node节点的前驱节点pred的waitStatus属性,来判断node节点可不可以被唤醒
* 第一次调用会把前一个节点的等待状态设置为SIGNAL,并返回false
* 第二次调用才会返回true
*
* 在本章节学习的内容中,总共有两个方法(shouldParkAfterFailedAcquire和unparkSuccessor)会涉及到对Node节点的waitestate属性进行修改,下面我们来简述一下大致变化流程:
* 假设当前持有锁的线程是T0,T1是等待队列中的第一个线程节点
* T0线程调用release()方法释放锁时,release()会判断等待队列的head指针节点的waitestate是否不等于0,如果不等于0成立,就会在unparkSuccessor()方法中将head指针节点的waitstate属性设置为0
* release()方法还会通过unparkSuccessor()将head.next,也就是等待队列第一个线程节点T1唤醒,然后这个线程节点就会去继续尝试获取锁
* 但是在非公平锁的场景下,T1获取锁可能会再次失败,被外来的T2线程插队,T2线程抢占到锁。这个时候T1就会需要再次被阻塞,然后再重新走一遍之前走过的流程
* 此时T1的waitestate = 0,所以在第一次进入到shouldParkAfterFailedAcquire()当法中会先将其设置为SIGNA(-1),然后返回到上层循环中再进行一轮循环,这个时候houldParkAfterFailedAcquire()才会返回true,然后执行后续的阻塞线程操作
* 所以这个T1线程会经历两轮循环,经历了waitestate由 -1 -> 0 -> -1的过程
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
/**
* 注意node的waitStatus字段我们在上面创建Node的时候并没有指定
* 也就是说使用的是默认值0
* 这里把各种等待状态再贴出来
*
* static final int CANCELLED = 1; //代表出现异常,中断引起的,需要废弃结束
* static final int SIGNAL = -1; //可被唤醒 指这个节点的下一个节点可被唤醒
* static final int CONDITION = -2; // 条件等待,这个是在条件锁中使用的Node状态
* static final int PROPAGATE = -3; // 传播,可以用来广播的状态
*/
// 获得node节点的前驱节点的waitStatus属性,用来标识node节点是不是可以被唤醒
int ws = pred.waitStatus;
// 如果node节点的前驱节点waitStatus属性为SIGNAL,说明node节点可以被唤醒,也就是可以被阻塞,返回true
if (ws == Node.SIGNAL)
return true;
// 如果node节点的前驱节点waitStatus大于0,说明前驱结点是出现异常的,也就是已取消状态,应该给移除掉(注意waitStatus = 1表示的是当前节点是异常状态,而不是后继节点是异常状态,所以当前节点node会被保留在队列中)
if (ws > 0) {
// 这里就进行一个循环,将node前面连续的,所有waitStatus > 0的结点都从队列中移除
do {
// 这个很好理解,就是一个简单的删除链表节点的过程
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 遍历到节点waitStatus状态不再大于0时,停止删除操作
// 更新node最新的前驱节点
pred.next = node;
// 如果是其他情况,就将前驱结点的waitStatus设置为SIGNAL,也就是将node节点设置为可唤醒状态。
// 但是本次调用还是会返回false,在上一层调用shouldParkAfterFailedAcquire的位置是一个循环,所以返回上一层进行第二轮循环的时候,再次调用shouldParkAfterFailedAcquire,就会直接进入到第一个分支返回true了。
} else {
/**
* 如果前一个节点的状态小于等于0,则把其状态设置为等待唤醒
* 因为这一章节是以互斥锁为例进行讲解,所以这里可以简单地理解为把初始状态0设置为SIGNAL
* CONDITION是条件锁的时候使用的
* PROPAGATE是共享锁使用的
*/
// 原子操作,将前驱节点的waitStatus设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// AbstractQueuedSynchronizer.parkAndCheckInterrupt()
// 将当前线程进行阻塞
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
// 底层调用的是Unsafe的park()方法
LockSupport.park(this);
/**
* 这个作用就是告知上一层这个线程是不是被中断信号唤醒的
* 因为Park阻塞线程唤醒有两种方式:
* 1、中断(interrupt)
* 2、release()
*
* 如果当前线程是中断信号唤醒的,我们需要将这个中断信号向上层传递,告知上一层这个线程被中断了,当这个线程成功获取到锁之后,需要去响应中断
*/
/**
* 返回当前线程的中断信号,并且将中断信号清除
*
* 之前有一个疑问,这里向上层返回当前线程的中断信号,为什么不直接使用isInterrupted()来得到中断信号向上返回,而非得使用interrupted()来获取中断信号向上层返回
* 当时觉得用interrupted()获取了中断信号,并且将其清除。将中断信号传递到外层之后,还需要再次使用selfInterrupt()来重新设置中断标志位,多此一举
* 但实际这里使用interrupted()是非常正确的,因为如果一个线程的中断标志位为true,那么LockSupport.park()对该线程将会失去效果,也就是无法对该线程进行阻塞。
* 当一个线程被唤醒以后,并不一定能成功获取锁,可能会被再次阻塞:
* 1、当公平锁等待队列中的非头部线程节点被中断信号唤醒之后,它并不能成功获取到锁,所以会被再次阻塞
* 2、当非公平锁等待队列中的线程节点(头部线程节点或非头部线程节点)被唤醒(release唤醒或中断信号唤醒)都不能保证其一定能获取到锁,都有可能再次被阻塞
* 所以必须保证下次仍能成功将其阻塞才可以,这里必须先暂时将线程的中断信号清除
*/
return Thread.interrupted();
}
// AbstractQueuedSynchronizer.cancelAcquire()
// 取消当前线程节点node获取锁,并且将当前节点的waitStatus设置为CANCELLED取消状态。但是此时并不会将该节点移出队列,在未来该节点的后继节点进入到shouldParkAfterFailedAcquire()方法时,才就会将该节点给移出队列
// acquireQueued()方法中如果线程没有成功获取锁,try{}代码块中的代码中执行结束,在finally{}中就会执行这个方法,将当前线程的node设置为取消状态
private void cancelAcquire(Node node) {
if (node == null)
return;
// 将node节点的线程属性置为空
node.thread = null;
// 将当前节点前面所有连续的waitStatus > 0 的取消状态的节点都从队列中删除
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 删除取消状态的节点后,将node节点的前驱结点的next节点赋值给predNext
Node predNext = pred.next;
// 将当前节点设置为取消状态
node.waitStatus = Node.CANCELLED;
// 如果当前节点node是队列的尾部节点,则将node移除掉(使用原子操作,防止并发异常)
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
// AbstractQueuedSynchronizer.selfInterrupt()
// 在acquire()中调用该方法,只有当线程在队列中阻塞等待时,被中断信号唤醒,等到这个线程成功获取到锁之后,就会向上层传递中断信号,进而会调用该方法来响应中断
static void selfInterrupt() {
// 设置当前线程的中断信号为true
Thread.currentThread().interrupt();
}
下面我们看一下主要方法的调用关系:
ReentrantLock#lock() // 获取锁
->ReentrantLock.FairSync#lock() // 公平模式获取锁
->AbstractQueuedSynchronizer#acquire() // AQS的获取锁方法
->ReentrantLock.FairSync#tryAcquire() // 尝试获取锁
->AbstractQueuedSynchronizer#addWaiter() // 尝试获取锁失败,将线程添加到等待队列
->AbstractQueuedSynchronizer#enq() // 如果上一个方法入队没有成功,则在这个方法中自旋,不断尝试入队
->AbstractQueuedSynchronizer#acquireQueued() // 这个方法就是来将入队的线程阻塞,并且里面有个for()循环,线程被唤醒后会在该方法中自旋继续尝试获取锁
->AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire() // 检查当前节点是否可以被唤醒,进而决定能否对其进行阻塞
->AbstractQueuedSynchronizer#parkAndCheckInterrupt() // 真正线程阻塞的方法
获取锁的主要过程大致如下:
- 尝试获取锁,如果获取到了就直接返回了;
- 尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;
- 然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;
- 如果再次失败,再调用shouldParkAfterFailedAcquire()判断线程是否能被唤醒,如果不能被唤醒则尝试将节点设置为可唤醒状态(SIGNAL),只有这样才可以将此线程进行阻塞;
- 调用parkAndCheckInterrupt()阻塞当前线程;
- 如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;
- 如果不成功,再次阻塞,重复(3)(4)(5)直到成功获取到锁。
以上就是整个公平锁获取锁的过程,下面我们看看非公平锁是怎么获取锁的。
2 非公平锁
这里我们假设ReentrantLock的实例是通过以下方式获得的(ReentrantLock默认是非公平锁):
ReentrantLock reentrantLock = new ReentrantLock();
// ReentrantLock默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
下面的是加锁的主要逻辑:
// ReentrantLock.lock()
public void lock() {
sync.lock();
}
// ReentrantLock.NonfairSync.lock()
// 这个方法在公平锁模式下是直接调用的acquire(1);
final void lock() {
// 和公平锁相比,这里会先直接尝试CAS更新状态变量,如果此时锁没有被线程占用,就能成功将状态变量由0变为1,即成功获取到锁
if (compareAndSetState(0, 1))
// 如果更新成功,说明获取到锁,把当前线程设为该锁的独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果没有成功获取到锁,则执行acquire(1)方法,
acquire(1);
}
// AbstractQueuedSynchronizer.acquire()
// 这里面的操作和公平锁都是一样的
public final void acquire(int arg) {
// 调用尝试获取锁的方法,如果尝试获取锁失败,则将该线程添加到等待队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// ReentrantLock.NonfairSync.tryAcquire()
// 这里和公平锁不太一样,这里调用的是非公平锁的acquires方法:nonfairTryAcquire()
protected final boolean tryAcquire(int acquires) {
// 调用父类的方法
return nonfairTryAcquire(acquires);
}
// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前state值
int c = getState();
if (c == 0) {
// 如果状态变量的值为0,再次尝试CAS更新状态变量的值
// 相对于公平锁模式少了!hasQueuedPredecessors()条件,非公平锁不需要判断是是否有线程在等待,而是直接去尝试获取锁
if (compareAndSetState(0, acquires)) {
// 将当前线程设置为此锁的占有者
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程就是正在持有此资源的线程,则可直接获取,也就是实现了可重入锁
else if (current == getExclusiveOwnerThread()) {
// 重入锁,累加state值
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置state的值
setState(nextc);
return true;
}
// 没有成功获取锁,则返回false
return false;
}
除了上述代码以外,其他代码流程非公平锁和公平锁都是一样的。
3 公平锁和非公平锁总结
3.1 公平锁和非公平锁实现源码的区别
公平锁的 lock 方法:
// 继承Sync抽象类的ReentrantLock内部类
static final class FairSync extends Sync {
final void lock() {
// 1. 预期值是1,说明这是一个独享锁。这里会直接进入acquire(),而不是和非公平锁一样先尝试CAS抢占一次资源
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
// 调用尝试获取锁的方法,如果尝试获取锁失败,则将该线程添加到等待队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前state值
int c = getState();
if (c == 0) {
// 2. 和非公平锁相比,这里多了一个判断:是否有线程在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 将当前线程设置为此资源的占有者
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程就是正在持有此资源的线程,则可直接获取,也就是实现了可重入锁
else if (current == getExclusiveOwnerThread()) {
// 重入锁,累加state值
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置state的值
setState(nextc);
return true;
}
return false;
}
}
非公平锁的 lock 方法:
// 继承Sync抽象类的ReentrantLock内部类
static final class NonfairSync extends Sync {
final void lock() {
// 1. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
if (compareAndSetState(0, 1))
// 将当前线程设置为此资源的占有者
setExclusiveOwnerThread(Thread.currentThread());
else
// 预期值是1,说明这是一个独享锁
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
// 调用尝试获取锁的方法,如果尝试获取锁失败,则将该线程添加到等待队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 非公平锁的tryAcquire()使用的是非公平的acquires方法:nonfairTryAcquire()
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* 这是ReentrantLock的抽线内部类中的方法,抽象类中既可以有不实现的抽象方法,也可以有已经实现好的普通方法,nonfairTryAcquire()就是抽象类中已经实现好的普通方法
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前state值
int c = getState();
if (c == 0) {
// 2. 非公平锁不需要判断是是否有线程在等待,而是直接去尝试获取锁
if (compareAndSetState(0, acquires)) {
// 将当前线程设置为此资源的占有者
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程就是正在持有此资源的线程,则可直接获取,也就是实现了可重入锁
else if (current == getExclusiveOwnerThread()) {
// 重入锁,累加state值
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置state的值
setState(nextc);
return true;
}
// 没有成功获取锁,则返回false
return false;
}
3.2 公平锁和非公平锁区别
公平锁和非公平锁只有两处不同:
- 非公平锁在调用 lock() 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。但是公平锁会先判断等待列中是否有处于等待状态的线程,如果有的话,就乖乖加入到等待线程中去排队,而不能直接插队获取锁。
- 非公平锁在调用 lock() 中的第一次CAS 失败后,和公平锁一样都会进入到acquire()方法,然后就会进入到tryAcquire()方法,在 tryAcquire 方法中,非公平锁调用的是自己实现好的nonfairTryAcquire()非公平tryAcquire方法,如果发现锁这个时候被释放了(state == 0),非公平锁就会直接 CAS 抢锁,不会管当前等待队列中有没有等待线程。但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。如果非公平锁这两次 CAS获取锁 都不成功,那么后面非公平锁和公平锁的流程就是一样的,都要进入到阻塞队列等待唤醒。相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
3.3 为什么ReentrantLock默认采用的是非公平模式?
因为非公平模式效率比较高。之所以非公平效率高,是因为非公平模式会在一开始就尝试两次获取锁,如果当时正好state的值为0,它就会成功获取到锁,少了排队导致的阻塞/唤醒过程,并且减少了线程频繁的切换带来的性能损耗。
但是非公平模式也有一些弊端,比如非公平模式有可能会导致一开始排队的线程一直获取不到锁,导致线程饿死。
相关文章: 【并发基础】CAS(Compare And Swap)操作的底层原理以及应用详解
【并发基础】AQS(Abstract Queued Synchronizer)框架的使用和实现原理详解
【并发编程】Java中的锁有哪些?各自都有什么样的特性?
【并发编程】Lock接口
【并发编程】Condition条件锁源码详解