ReentrantLock 原理


在这里插入图片描述
在这里插入图片描述


1. ReentrantLock 原理概述

在这里插入图片描述
ReentrantLock内部有三个内部类Sync(同步器),NonfairSync(非公平锁),FairSync(公平锁),另外实现了Lock接口。

  • Sync(同步器)继承了AQS类(AbstractQueuedSynchronizer);
  • NonfairSync(非公平锁)继承了 Sync类;
  • FairSync(公平锁)继承了Sync类。

所以ReentrantLock是基于AQS实现的,AQS是java并发包的基础类。里面维护者一个同步状态state,和一个同步队列FIFO以及操作state和同步队列的方法。

如果有一个线程过来尝试用ReentrantLock的lock()方法进行加锁,会发生什么事情呢?

AQS对象内部有一个核心的变量叫做state,是int类型的并且加了volatile关键字,代表了加锁的状态。初始状态下,这个state的值是0。

AQS内部还有一个关键变量ExclusiveOwnerThread,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null
在这里插入图片描述
默认非公平锁NonfairSync实现加锁:刚开始线程1去调用ReentrantLock的lock()方法(这个lock方法是重写Lock接口的方法)加锁,通过底层用到Sync、AQS的类,然后通过CAS操作将state从0变为1,如果之前没人加过锁,刚开始CAS操作加锁成功,因为一开始的state为0,加锁成功后将ExclusiveOwnerThread置为线程1
在这里插入图片描述

  • 从上面发现ReentrantLock借助内部类AQS实现加锁,自己只是AQS的最外层API,AQS作为J.U.C并发包的核心类,里面有关于volatile关键字的state,ExclusiveOwnerThread加锁状态,还有FIFO的阻塞队列(同步队列),相当于monitor的EntryList。内核中的锁机制实现都是依赖AQS组件的。

  • 可重入锁就是线程1每次都给同一对象加锁,每次加锁时都判断当前线程是不是自己,是自己,那每次锁重入时,state的值就累加1,ExclusiveOwnerThread还是当前线程1。

接着,如果线程1加锁了之后,线程2跑过来加锁会怎么样?
线程2跑过来一下看到,state的值不是0啊?所以CAS操作将state从0变为1的过程会失败,因为state的值当前为1,说明已经有人加锁了!(当前ExclusiveOwnerThread为线程1)
接着线程2会看一下,是不是自己之前加的锁啊?当然不是了,“加锁线程”这个变量明确记录了是线程1占用了这个锁,所以线程2此时就是加锁失败。
在这里插入图片描述
当线程1一直没有主动释放锁的话,线程2加锁失败后,会再经历4次加锁失败,最后进入AQS的同步队列FIFO进行阻塞(挂起),等待被唤醒或者被打断再次重新获取资源竞争锁,反正一唤醒就CAS操作竞争锁,搞不好哪一次就竞争成功加上了锁。
在这里插入图片描述
接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”ExclusiveOwnerThread变量也设置为null!
在这里插入图片描述
接下来,会从同步队列(阻塞队列)的队头唤醒线程2重新尝试加锁。这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从阻塞队列中出队了。
在这里插入图片描述
记住:FIFO队列是满足先进先出的原则的,每次拿锁时,老二线程会先获取锁(因为head头结点是哨兵结点,哑结点,不放线程,只能老二优先级最高)。
但是体现非公平锁的机制是在同步队列老二线程准备获取锁的同时,被其他非同步队列的线程抢先获得锁了,这时老二线程又会被阻塞住。(非公平就是不管AQS队列)。
而公平锁就是先检查 AQS 队列中是否有前驱节点, 没有才去竞争。

ReentrantLock就是使用AQS而实现的一把锁,它实现了可重入锁,公平锁和非公平锁。它有一个内部类用作同步器是Sync,Sync是继承了AQS的一个子类,并且公平锁和非公平锁是继承了Sync的两个子类。ReentrantLock的原理是:假设有一个线程A来尝试获取锁,它会先CAS修改state的值,从0修改到1,如果修改成功,那就说明获取锁成功,设置加锁线程为当前线程。如果此时又有一个线程B来尝试获取锁,那么它也会CAS修改state的值,从0修改到1,因为线程A已经修改了state的值,那么线程B就会修改失败,然后他会判断一下加锁线程是否为自己本身线程,如果是自己本身线程的话它就会将state的值直接加1,这是为了实现锁的可重入。如果加锁线程不是当前线程的话,那么就会将它生成一个Node节点,加入到等待队列的队尾,直到什么时候线程A释放了锁它会唤醒同步队列队头的线程。这里还要分为公平锁和非公平锁,默认为非公平锁,公平锁和非公平锁无非就差了一步。如果是公平锁,此时又有外来线程尝试获取锁,它会首先判断一下同步队列是否有前驱节点,如果有前驱节点,就说明同步队列不为空,有等待获取锁的线程,那么它就不会去同步队列中抢占cpu资源。如果是非公平锁的话,它就不会判断同步队列是否有第一个节点,它会直接前往同步对列中去抢占cpu资源。

2. ReentrantLock 源码分析

ReentrantLock默认是采用非公平锁

public ReentrantLock() {
sync = new NonfairSync();
}

因为ReentrantLock默认是采用非公平锁,那么我们就把非公平锁的加锁源码和解锁源码先粘贴出来,之后再分步分析,其实掌握了非公平锁的源码,之后的公平锁、可重入、可打断、不可打断、条件变量原理都差不多类似代码。

加锁源码

//NonfairSyn是ReentrantLock非公平锁 Sync 继承自 AQS
//ReentrantLock的内部类 NonfairSync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 加锁实现
final void lock() {
// 首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果尝试失败,进入 ㈠
acquire(1);
}
// ㈠ AQS里 继承过来的方法acquire, 方便阅读, 放在此处
public final void acquire(int arg) {
// ㈡ tryAcquire
if (//这个tryAcquire会进入AQS里的tryAcquire,
//但是一般是子类去重写这个方法,这里是非公平锁,去调用子类NonfairSync的tryAcquire方法
!tryAcquire(arg) &&
// 当 tryAcquire 返回为 false 时, 先调用 addWaiter ㈣, 接着 acquireQueued ㈤
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}
// ㈡ 进入 ㈢
//子类NonfairSync重写过后的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// ㈢ Sync 里继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果还没有获得锁
if (c == 0) {
// 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
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");
setState(nextc);
return true;
}
// 获取失败, 回到调用处
return false;
}
// ㈣ AQS 继承过来的方法, 方便阅读, 放在此处
private Node addWaiter(Node mode) {
// 将当前线程关联到一个 Node 对象上, 模式为独占模式
Node node = new Node(Thread.currentThread(), mode);
// 如果 tail 不为 null, cas 尝试将 Node 对象加入 AQS 队列尾部
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
// 双向链表
pred.next = node;
return node;
}
}
// 尝试将 Node 加入 AQS, 进入 ㈥
enq(node);
return node;
}
// ㈥ AQS 继承过来的方法, 方便阅读, 放在此处
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// 还没有, 设置 head 为哨兵节点(不对应线程,状态为 0)
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
// cas 尝试将 Node 对象加入 AQS 队列尾部
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// ㈤ AQS 继承过来的方法, 方便阅读, 放在此处
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 上一个节点是 head, 表示轮到自己(当前线程对应的 node)了, 尝试获取
if (p == head && tryAcquire(arg)) {
// 获取成功, 设置自己(当前线程对应的 node)为 head
setHead(node);
// 上一个节点 help GC
p.next = null;
failed = false;
// 返回中断标记 false
return interrupted;
}
if (
// 判断是否应当 park, 进入 ㈦
shouldParkAfterFailedAcquire(p, node) &&
// park 等待, 此时 Node 的状态被置为 Node.SIGNAL ㈧
parkAndCheckInterrupt()
) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// ㈦ AQS 继承过来的方法, 方便阅读, 放在此处
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取上一个节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) {
// 上一个节点都在阻塞, 那么自己也阻塞好了
return true;
}
// > 0 表示取消状态
if (ws > 0) {
// 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 这次还没有阻塞
// 但下次如果重试不成功, 则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//ws=0是最后一个状态,CAS后返回false,进行下一次for(;;)尝试加锁
return false;
}
// ㈧ 阻塞当前线程
private final boolean parkAndCheckInterrupt() {
//park阻塞住后,就会在同步队列休眠,
//除非被前驱结点(-1)唤醒或interrupt打断
LockSupport.park(this);
return Thread.interrupted();
}
}

解锁源码

// Sync 继承自 AQS
static final class NonfairSync extends Sync {
// 解锁实现
public void unlock() {
sync.release(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean release(int arg) {
// 尝试释放锁, 进入 ㈠
if (tryRelease(arg)) {
// 队列头节点 unpark
Node h = head;
if (
// 队列不为 null
h != null &&
// waitStatus == Node.SIGNAL 才需要 unpark
h.waitStatus != 0
) {
// unpark AQS 中等待的线程, 进入 ㈡
unparkSuccessor(h);
}
return true;
}
return false;
}
// ㈠ Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
private void unparkSuccessor(Node node) {
// 如果状态为 Node.SIGNAL 尝试重置状态为 0
// 不成功也可以
int ws = node.waitStatus;
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
// 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的
Node s = node.next;
// 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点
if (s == null || s.waitStatus > 0) {
s = 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);
}
}

2.1. 非公平锁实现原理

加锁流程

lock()方法的逻辑: 多个线程调用lock()方法, 如果当前state为0, 说明当前没有线程占有锁, 那么只有一个线程会CAS获得锁,state更新为1, 并设置此线程为独占锁线程。那么其它线程会调用acquire方法来竞争锁(后续会全部加入同步队列中自旋或挂起)。当有其它线程A又进来想要获取锁时, 恰好此前的某一线程恰好释放锁, 那么A会恰好在同步队列中所有等待获取锁的线程之前抢先获取锁(非公平体现,不管AQS同步队列)。也就是说所有已经在同步队列中的尚未被 取消获取锁 的线程是绝对保证串行获取锁(FIFO保证先进先出,老二线程优先级最高,头结点不放线程,哑线程),而其它新来的却可能抢先获取锁。

首次调用lock加锁

public ReentrantLock() {
sync = new NonfairSync();
}
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
//......
} 
//非公平锁继承自Sync,Sync继承自AQS,在AQS中定义了加锁释放锁,同步状态,同步队列等
static final class NonfairSync extends Sync {
//加锁
final void lock() {
//利用CAS将status=0,改成status=1
if (compareAndSetState(0, 1))
//如果CAS成功,就会将Owner线程指向当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//如果CAS失败,代表出现竞争,就会进入这个方法
acquire(1);
} 
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
}

在没有竞争时:线程Thread-0通过CAS将同步状态从0设置为1,如果成功,就会将加锁线程设置为当前线程
在这里插入图片描述
当第一个竞争出现时:Thread-1开始竞争加锁
在这里插入图片描述
Thread-1竞争失败,因为state已经为1,不可能从0变为1,CAS就会操作失败,进入acquire(1)方法

else
//如果CAS失败,代表出现竞争,就会进入这个方法
acquire(1);

2.1.1 acquire(1)

线程Thread-1同样想通过CAS操作将status从0修改为1,但是此时CAS失败,就会进入acquire(1)方法:

//如果出现竞争,就会进入 acquire(1)方法
public final void acquire(int arg) {
if (
这个tryAcquire会进入AQS里的tryAcquire,
//但是一般是子类去重写这个方法,这里是非公平锁,去调用子类NonfairSync的tryAcquire方法
!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

首先进入AQS的tryAcquire方法,tryAcquire返回false后(子类重写该方法),其次进入AQS的addWaiter(Node.EXCLUSIVE)方法,最后进入acquireQueued(node,arg)


2.1.2 NonfairSync的 tryAcquire(1)

由于Thread-1执行tryAcquire(1)方法一定会返回false(因为CAS会失败)

//子类NonfairSync重写过后的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// ㈢ Sync 里继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果还没有获得锁
if (c == 0) {
// 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列
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");
setState(nextc);
return true;
}
// 获取失败, 回到调用处
return false;
}

tryAcquire 方法仍然尝试获取锁(快速获取锁机制),由于Thread-0持有锁,state还是1,tryAcquire返回false,!tryAcquire(arg)就为true。之后执行AQS的addWaiter(Node.EXCLUSIVE)将Thread-1包装成Node节点添加到AQS同步队列尾部。
Node.EXCLUSIVE 为null表示这是独占锁,如果为读写锁,那就是 共享模式(shared)


2.1.3 addWaiter(Node mode)

!tryAcquire(arg)=true,接着就会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)中的addWaiter(Node.EXCLUSIVE)方法:

//创建一个节点对象,并把它加入到等待队列的队尾
private Node addWaiter(Node mode) {
//创建一个节点对象node
Node node = new Node(Thread.currentThread(), mode);
//指向tail节点的同步队列的尾节点
Node pred = tail;
//如果尾节点不为null,说明同步队列已经初始化
if (pred != null) {
//将节点对象node的prev指向pred(同步队列队尾的节点)
node.prev = pred;
/**
这里为什么需要CAS操作?(自我理解,多多指教)
如果线程Thread-0已经将同步状态从status=0修改成了status=1
此时又有其他线程Thread-1,Thread-2,Thread-3,...,来进行CAS操作,试图将status=0改成1,但是Thread-1,Thread-2,Thread-3,...,都会导致CAS操作失败。
被阻塞的线程会被构造成一个节点对象通过CAS操作加入到同步队列的尾部,之所以使用CAS操作,是因为 tail指向该节点Thread-3时,可能Thread-2已经更改了tail的值,主要是为了保证线程安全和提高效率
*/
/**
因为tail是加了volatile关键字的,因此其数据的更改会被同步到主存中,被其他线程可见,在setTail之前会比较获取到的旧值pred和重新从主存中获取的新值进行比较:如果一致,就将node更新为pred,那么此时新的节点node指向tial节点,如果不一致,CAS失败,就是说将线程加入到同步队列失败
*/
//通过CAS操作设置新节点node为尾节点,指向tail节点,快速添加尾节点
if (compareAndSetTail(pred, node)) {
//CAS成功后,node成为尾节点,将pred节点的next指针指向node
pred.next = node;
return node;
}
} 
//1、如果尾节点为空,说明队列还未初始化,需要初始化head节点并将node加入到队列尾部
//2、如果将节点快速添加到队尾中,没添加进去,会继续执行enq(node)方法以CAS自旋的方式添加,知道添加进去为止
enq(node);
return node;
}

addWaiter(Node mode)方法的作用就是将当前线程构造成一个节点对象然后加入到同步队列的队尾,这个方法有两层逻辑:

  1. 如果该线程是第一个出现竞争的线程Thread-1,那么就说明队列尾空,会直接执行enq(node)方法,先初始化同步队列,构造一个哨兵节点,该节点不关联任何线程,然后将当前线程节点加入到队列的尾部,同步器通过“死循环”for(;;)来保证节点正确被添加,在“死循环”中只有通过CAS将节点设置为尾节点之后,当前线程才能从该方法中返回,否则,当前线程不断通过CAS尝试设置。
  2. 如果该线程不是第一个出现竞争的线程Thread-2,那么说明同步队列不为null,首先会执行该方法快速将该节点添加到同步对队列的队尾,如果没有添加成功,会继续执行enq(node)方法,以CAS自旋将该节点添加到队尾中。

总结 addWaiter 逻辑:

  1. Node包装当前线程
  2. pred 尾指针不为null,即队列不为空, 则快速CAS将自己设为新的tail
  3. 如果队列为空, 则调用enq强制入队
  4. 如果CAS设置失败,说明在其它线程入队节点争抢了tail,则此线程只能调用enq强制入队

如果是上述3.4条件则会进入 enq(final Node node)方法强制入队尾


2.1.4 enq(final Node node)
  1. 由于Thread-1是第一个要竞争锁的线程,因此同步队列尾节点为null,说明队列还未初始化,需要初始化head节点,然后将当前节点添加到同步队列的队尾
  2. 将Thread-1节点添加到队尾中没有添加成功
    以上两种情况都会执行enq(final Node node)方法:
//初始化同步队列,设置头节点和尾节点
/**
同步器通过“死循环”来保证节点正确被添加,在“死循环”中只有通过CAS将节点设置为尾节点之后,当前线
程才能从该方法中返回,否则,当前线程不断通过CAS尝试设置。
*/
private Node enq(final Node node) {
//死循环,CAS自旋,多次尝试,直到成功为止
for (;;) {
//指向tail节点的同步队列的尾节点
Node t = tail;
//如果t=null,说明队列还没有初始化,需要先通过CAS自旋设置一个头结点head
if (t == null) {
if (compareAndSetHead(new Node()))
//该head 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
tail = head;
//如果t!=null,说明队列已经初始化,那么会让节点t以自旋的方式加入到队列的尾部
} else {
//将当前节点的prev指向t
node.prev = t;
//通过CAS的方式将t节点更新为node节点,就是将node节点加入到队列的尾部,此时
node节点指向tail
if (compareAndSetTail(t, node)) {
//如果CAS成功,说明node节点成功添加到同步队列尾部
t.next = node;
return t;
}
}
}
}

方法内是一个 for(;;),看来退出的条件只能是当前线程入队成功。之前也提到过,只有在产生锁竞争了,才会去初始化链表头节点。如果队列为空,初始化头尾节点,然后后续循环会走到else,else的逻辑和上线addWaiter方法的CAS入队的逻辑一样,只不过这里套在for循环里,直到入队成功才退出循环。

总结一下:addWaiter方法的实现比较简单且实现功能明了:把当前线程构造成一个节点node对象,强制加入到同步队列队尾。(不会加不进去,因为addWaiter方法内调用enq(final Node node)方法强制入队尾)
在这里插入图片描述


2.1.5 acquireQueued()

当上述addWaiter()入AQS同步队列完成后,就可以执行acquireQueued()方法,这个方法是再次尝试从同步队列中获取锁,如果获取锁再次失败就会进入shouldParkAfterFailedAcquire()判断是否在同步队列中阻塞(挂起)该线程,如果应该阻塞就再执行parkAndCheckInterrupt()将该线程在同步队列中挂起

final boolean acquireQueued(final Node node, int arg) {
//标记是否成功获取锁
boolean failed = true;
try {
//标记是否被打断
boolean interrupted = false;
//死循环,调用tryAcquire(arg)方法,CAS自旋不断尝试获取锁
for (;;) {
//获取node节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点为头节点Head,即该结点已成老二,那么便有资格去尝试获取锁
//可能是老大释放完资源唤醒自己的,当然也可能被interrupt了
//执行tryAcquire(arg)方法尝试获取锁
if (p == head && tryAcquire(arg)) {
//如果该节点获取到了锁,就将当前节点设置为头节点
setHead(node);
//原head节点出队,在某个时间点被GC回收
p.next = null; // help GC
//获取锁成功
failed = false;
//返回等待的过程中是否被中断过
return interrupted;
} 
//如果上一步获取锁失败后,判断是否可通过park()进入waiting状态,直到被unpark()
//如果不可中断的情况下被中断了,会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
interrupted = true;
}
} finally {
if (failed)
//若等待过程中没有成功获取资源(timeout,或可中断的情况下被中断了),取消结点在队列中的等待。
cancelAcquire(node);
}
}

failed 标记最终获取锁是否成功, interrupted标记是否曾被挂起过(阻塞)。注意到for(;;) 跳出的唯一条件就是if (p == head && tryAcquire(arg)) 即当前线程结点的前驱结点是头结点且获取锁成功。从这里我们应该看到,这是一个线程第三次又想着尝试快速获取锁:虽然此时该节点已被加入等待队列,在进行睡眠之前又通过(p == head && tryAcquire(arg)方法看看能否获取锁。也就是说只有该线程结点的所有 有效的前置结点都拿到过锁了,当前结点才有机会争夺锁,如果失败了那就通过shouldParkAfterFailedAcquire方法判断是否应该挂起当前结点,等待响应中断。


2.1.6 shouldParkAfterFailedAcquire()

这里还有一次(p == head && tryAcquire(arg)尝试加锁:因为前驱结点的waitStatus先是0,CAS置为-1后,返回false后,不满足阻塞条件,再一次for(;;)经过tryAcquire(arg)尝试加锁,之后加锁失败,再次shouldParkAfterFailedAcquire(),此时waitStatus为-1,返回true,阻塞该线程Thread-1,不再进入死循环获取锁,除非被唤醒或打断。

/**
判断当前线程获取锁失败之后是否需要挂起.
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的等待状态waitStatus
int ws = pred.waitStatus;
/*如果前驱节点的等待状态为Node.SIGNAL=-1,就说明当前节点可以休息了
线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如
果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的
前驱是否状态符合要求,若符合则返回true。然后调用parkAndCheckInterrupt,将自己挂起。*/
if (ws == Node.SIGNAL)
return true;
//如果ws > 0,前驱节点的状态为CANCELLED=1
//如果ws>0说明前置结点是被自己取消获取同步的结点(只有线程本身可以取消自己)。
//那么do while循环一直往头结点方向找waitStatus < 0的节点;
//含义就是去除了FIFO队列中的已经自我取消申请同步状态的线程。
if (ws > 0) {
do {
//从队尾向前寻找第一个状态不为CANCELLED=1的节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果ws<=0,那么就通过CAS将pred的前驱节点的等待状态改成Node.SIGNAL=-1
//入队后的线程waitStatus默认为0,所以改为-1后,返回false,再一次for循环后线程才能阻塞住
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
 return false;
}

2.1.7 parkAndCheckInterrupt()

当shouldParkAfterFailedAcquire()方法返回true时,执行parkAndCheckInterrupt()方法阻塞线程

// 挂起当前线程,返回线程中断状态并重置
private final boolean parkAndCheckInterrupt() {
//调用park()方法阻塞当前线程
LockSupport.park(this);
//返回中断状态,如果没有线程打断该线程的休息,就会返回false,一旦有其他线程打断睡眠,就
会返回true
return Thread.interrupted();
}

总结上述3个方法acquireQueued()、shouldParkAfterFailedAcquire()、parkAndCheckInterrupt()
处于阻塞队列中的线程Thread-1尝试获取锁的过程:

  1. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
  2. 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
  3. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
  4. shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败
  5. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回true
  6. 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)

在这里插入图片描述
执行acquireQueued()方法内的第一次for循环,第一次尝试获取锁:
获取Thread-1线程节点的前驱节点,并判断该节点是不是头结点Head,如果是的话,Thread-1线程就会继续执行tryAcquire(1)方法尝试获取锁,很明显CAS会失败,因为Thread-0已经获取了同步资源。失败后接着会向下执行shouldParkAfterFailedAcquire()方法,该方法首先会获取前驱节点的等待状态waitStatus:
如果waitStatus=-1,就说明当Thread-1节点的前驱节点获取锁并且出队后会把Thread-1唤醒(“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”),同时return true;接着会继续执行parkAndCheckInterrupt()方法让Thread-1线程挂起(调用park()方法让该线程阻塞住,不用一直CAS自旋尝试获取锁)。
很明显Thread-1线程的前驱节点的waitStatus=0,那么就会执行 compareAndSetWaitStatus(pred,ws, Node.SIGNAL); 方法将waitStatus设置为-1,同时 return false


CAS操作后,你看Thread-1的前驱结点waitStatus为-1了,有唤醒后继节点的功能了,之后调用parkAndCheckInterrupt()方法让Thread-1线程阻塞,Thread-1结点变成灰色,如图
在这里插入图片描述
执行acquireQueued()方法内的第二次for循环,第二次尝试获取锁:
首先获取Thread-1线程节点的前驱节点,并判断该节点是不是head,如果是的话,Thread-1线程会继续执行tryAcquire(1)方法尝试获取锁,由于Status=1,因此获取CAS失败,就是说该线程有一次通过CAS获取锁失败。失败后会接着继续向下执行shouldParkAfterFailedAcquire()方法,该方法首先会获取前驱节点的等待状态waitStatus,进行判断:
经过上述CAS操作,很明显waitStatus=-1,就说明Thread-1节点的前驱节点获取锁并出队后会把Thread-1线程唤醒,同时return true接着会继续执行parkAndCheckInterrupt()方法让Thread-1线程挂起(调用park()方法让该线程阻塞住,不用一直CAS自旋尝试获取锁)。在挂起的过程中,如果没有被其他线程打断,就会一直处于阻塞状态。如果在阻塞的过程中被其他线程打断了,那么 return Thread.interrupted(); 就会返回true。继而acquireQueued()方法就会继续向下执行 interrupted = true; ,同时被打断的线程会继续执行for循环尝试获取锁,如果获取不到会继续调用park()进入阻塞状态。


再次有多个线程经历上述过程竞争失败,变成这个样子:
由图中可知,每当一个线程在同步队列阻塞,它的前驱结点的waitStatus=-1,当前节点默认为0,当下一次再有线程阻塞时,那么当前线程变为下一个线程的前驱结点才会CAS从ws变为-1。
在这里插入图片描述


释放锁流程

2.1.8 release(int arg)

Thread-0 释放锁,进入 tryRelease 流程

// 解锁实现
public void unlock() {
sync.release(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean release(int arg) {
// 尝试释放锁, 进入 ㈠
if (tryRelease(arg)) {
// 队列头节点 unpark
Node h = head;
if (
// 队列不为 null
h != null &&
// waitStatus == Node.SIGNAL 才需要 unpark
h.waitStatus != 0
) {
// unpark AQS 中等待的线程, 进入 ㈡
unparkSuccessor(h);
}
return true;
}
return false;
}

2.1.9 tryRelease(int arg)
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}

Sync子类实现了AQS类的tryRelease方法

// ㈠ Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。Thread-0 释放锁,进入tryRelease 流程,如果成功设置 exclusiveOwnerThread 为 null、state = 0
在这里插入图片描述


2.1.10 unparkSuccessor()

当tryRelease()方法返回true后,则表明Thread-0线程释放锁成功,进入unparkSuccessor()唤醒老二线程(优先级最高)

// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
private void unparkSuccessor(Node node) {
// 如果状态为 Node.SIGNAL 尝试重置状态为 0
// 不成功也可以
int ws = node.waitStatus;
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
// 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的
Node s = node.next;
// 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点
if (s == null || s.waitStatus > 0) {
s = 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);
}

总结上述释放锁并唤醒阻塞队列线程的三个方法:
release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
在release(1)中,如果当前队列不为 null,并且 head 的waitStatus = -1,进入 unparkSuccessor 流程,找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1,回到 Thread-1 的 acquireQueued 流程 ,s被唤醒后,进入 if (p == head &&tryAcquire(arg)) 的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了。
在这里插入图片描述
如果加锁成功(没有竞争),会设置exclusiveOwnerThread 为 Thread-1,state = 1。head 指向刚刚Thread-1 所在的 Node,该 Node 清空 Thread。原本的 head 因为从链表断开,而可被垃圾回收 。

如果这时候有其它线程来竞争(非公平锁的体现),例如这时有 Thread-4 来了
在这里插入图片描述
如果不巧又被 Thread-4 占了先,Thread-4 被设置为 exclusiveOwnerThread,state = 1。Thread-1再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 。


整个非公平锁加锁解锁过程
在这里插入图片描述


2.2 可重入原理

static final class NonfairSync extends Sync {
// ...
// Sync 继承过来的方法, 方便阅读, 放在此处
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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");
setState(nextc);
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}

原理:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占
用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。


2.3 公平锁和非公平锁的区别

两者的区别主要体现在加锁过程上的区别,即tryAcquire(1)方法的不同:
对于非公平锁的tryAcquire(1)方法:

final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state变量的值
int c = getState();
//如果state=0,说明还没有线程来占用锁
if (c == 0) {
//通过CAS将state=0设置为1
if (compareAndSetState(0, acquires)) {
//占用锁成功,将独占线程设置为当前线程
setExclusiveOwnerThread(current);
//同时返回true,代表获取锁资源成功
return true;
}
} 
//如果state=1,说明已经有其他线程获取了锁
//判断ExclusiveOwnerThread指向的线程是否是当前线程,就是说判断是不是自己占用了锁
else if (current == getExclusiveOwnerThread()) {
//如果是自己占有了锁,就说明出现了锁重入,那么就让state+=1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//更新state的重入次数
setState(nextc);
//返回true,同样表示占用锁成功
return true;
} 
//如果是其他线程占用了锁,那么获取锁失败,返回false.
return false;
}
  1. 检查state字段,若为0,表示锁未被占用,那么尝试占用
  2. 若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。
  3. 如果以上两点都没有成功,则获取锁失败,返回false。

非公平锁,首先是检查并设置锁的状态,如果state=0,就会去尝试加锁,并不会到同步队列中等待获取锁,这种方式会出现即使队列中有等待的线程,但是新的线程仍然会与同步队列中等待获取同步状态的线程竞争,所以新的线程可能会抢占已经在排队的线程的锁,这样就无法保证先来先服务,但是已经在AQS双向同步队列等待的线程们仍然是保证先来先服务的。

对于公平锁的tryAcquire(1)方法:

static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state变量的值
int c = getState();
//如果state=0
if (c == 0) {
//判断AQS队列中是否有前驱节点,如果没有就尝试获取锁资源
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//将当前线程设置为加锁线程
setExclusiveOwnerThread(current);
//结果返回true,表示加锁成功
return true;
}
} 
//判加锁线程是不是当前线程,可重入锁的原理
else if (current == getExclusiveOwnerThread()) {
//如果是,就将同步状态state+=1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//更新state字段表示锁重入的次数
setState(nextc);
//结果返回true,表示加锁成功
return true;
} 
//否则返回false
return false;
}
}

hasQueuedPredecessors() 方法:
和非公平锁对比多了这个方法逻辑, 也就意味着没有了新来线程插队的情况,保证了公平锁的获取串行化。

public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//h != t表示队列中有node,
return h != t &&
//表示队列中还没有老二,或者队列中的老二线程不是当前线程
((s = h.next) == null || s.thread != Thread.currentThread());
}

在公平锁中,每一次的tryAcquire都会检查CLH队列中是否仍有等待获取锁资源的线程,如果有返回false,进而自己也加入到等待队列中,通过这种方式来保证先来先服务的原则。


2.4 公平锁和非公平锁的释放

这两个其实是一样的

public void unlock() {
sync.release(1);
} 
public final boolean release(int arg) {
if (tryRelease(arg)) {
//找到头节点
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒下一个线程
unparkSuccessor(h);
return true;
} 
return false;
} 
//这个方法由子类去实现,这里公平锁和非公平锁的实现类逻辑相同
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}

自定义同步器ReentrantLock方法中 tryRelease()方法的具体实现类:

abstract static class Sync extends AbstractQueuedSynchronizer {
//尝试释放锁
protected final boolean tryRelease(int releases) {
//计算锁重入的次数
int c = getState() - releases;
//如果占有锁的线程不是当前线程,就抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果state=0,说明锁重入次数为0,表示释放成功
if (c == 0) {
free = true;
//将owner设置为null
setExclusiveOwnerThread(null);
}
//重置锁重入的次数
setState(c);
return free;
}
}

2.5 不可中断原理

在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。

/**
1、假如Thread-0线程正在阻塞状态,其他线程如Thread-1找到THread-0线程对象,
调用interrupt()方法将其打断,那么此时该方法的结果就会返回true
*/
private final boolean parkAndCheckInterrupt() {
//让该线程阻塞,如果打断标记为true,那么park()方法失效
LockSupport.park(this);
//返回是否被打断过,如果被打断了返回true,否则返回false,同时还会清除打断标记
return Thread.interrupted();
} 
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
/**
3、当线程被打断后,就会醒过来,醒过来以后将interrupted=true
然后重新去判断if (p == head && tryAcquire(arg)) 并尝试获取锁
只有获取锁成功,才会返回这个打断标记interrupted=true
*/
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
} /**
2、当parkAndCheckInterrupt()返回true后,就会继续向下执行,
将打断标记置为true,但是并用到这个打断标记,进而继续进行下一次的for循
环
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//将打断标记置为true,同时继续进行下一次的for循环
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
} 
public final void acquire(int arg) {
/**
4、如果acquireQueued()返回true,就会继续执行selfInterrupt()方法
*/
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} 
static void selfInterrupt() {
//重新产生一次中断,即改变了中断状态,interrupted=true
Thread.currentThread().interrupt();
}

总结:

  1. 假如Thread-0线程正在阻塞状态在同步队列中等待获取锁,其他线程如Thread-1找到Thread-0线程对象, 调用interrupt()方法将其打断,那么此时该方法的结果就会返回true
  2. 当parkAndCheckInterrupt()返回true后,就会继续向下执行,打断标记置为true,但是并用到这个打断标记,进而继续进行下一次的for循环(继续向下运行了,就是说被打断后并没有放弃等待获取锁资源)
  3. 当线程被打断后,就会醒过来,醒过来以后将interrupted=true,执行for循环, 然后重新去判断
    if (p == head && tryAcquire(arg)) 并尝试获取锁,只有获取锁成功,才会返回这个打断标记interrupted=true。
  4. 如果acquireQueued()返回true,就会继续执行selfInterrupt()方法,停止等待锁资源。

总的来说,如果其他线程打断了当前线程的阻塞状态,当前线程醒来后也不会放弃获取锁,而是继续向下执行,尝试获取锁,获取不成功,继续进入阻塞状态。即当前线程会一直在同步队列中死等,直到获取了锁为止。


2.6 可中断原理

static final class FairSync extends Sync {
//...
//调用ReentrantLock的lockInterruptibly()方法,就会执行AQS内的acquireInterruptibly
//其实ReentrantLock就相当于AQS的外层API
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
} 
public abstract class AbstractQueuedSynchronizer extends
AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquireInterruptibly(int arg) throws InterruptedException
{
//判断线程是否处于中断状态,如果处于就说明中断状态为true,同时清除打断标记,将中断状态设置为false
if (Thread.interrupted())
//如果线程处于打断状态,就会直接抛出异常,由于这个异常并没有别捕获,因此后面的代码就不会再执行
throw new InterruptedException();
//尝试获取锁,如果CAS失败就会继续向下执行(2)
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
//(2)
/**
2、如果parkAndCheckInterrupt()返回true,继续执行throw new
InterruptedException();
抛出异常就不会再进入死循环for(;;)中,这也是终止线程的方法之一
抛出异常后,那么线程就会停止等待获取锁,即不会再死等了
*/
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())
//抛出异常,直接结束,这样就可中断了
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
} /**
1、假如Thread-0线程正在阻塞状态,其他线程如Thread-1找到THread-0线程对象,
调用interrupt()方法将其打断,那么此时该方法的结果就会返回true
*/
private final boolean parkAndCheckInterrupt() {
//让该线程阻塞,如果打断标记为true,那么park()方法失效
LockSupport.park(this);
//返回是否被打断过,如果被打断了返回true,否则返回false,同时还会清除打断标记
return Thread.interrupted();
}
}

总结:

  1. 假如Thread-0线程正在阻塞状态在同步队列中等待获取锁,其他线程如Thread-1找到Thread-0线程对象, 调用interrupt()方法将其打断,那么此时该方法的结果就会返回true
  2. 当parkAndCheckInterrupt()返回true后,就会继续向下执行,抛出异常,不再执行for()循环,方法执行结束,线程终止,停止等待获取锁资源。

2.7 条件变量await()原理

每个条件变量其实就对应着一个条件队列,其实现类是ConditionObject,该类是AQS的内部类,一个
ConditionObject是一条等待队列。

1、await()方法
public class ConditionObject implements Condition, java.io.Serializable {
/** 等待队列的头节点 */
private transient Node firstWaiter;
/** 等待队列的尾节点 */
private transient Node lastWaiter;
public final void await() throws InterruptedException {
//如果等待的线程别打断,那么直接抛出异常InterruptedExceptio
if (Thread.interrupted())
throw new InterruptedException();
//当前线程封装成一个Node节点,并将该节点添加到ConditionObject的等待队列中
Node node = addConditionWaiter();
//接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁,返回节点状态
int savedState = fullyRelease(node);
int interruptMode = 0;
//判断当前节点在不在同步队列中,如果不在,说明是条件队列中的节点
while (!isOnSyncQueue(node)) {
//调用park()将当前线程阻塞
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
} 
//当前线程被唤醒后会再次尝试获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
}
2、addConditionWaiter()方法

调用addConditionWaiter()方法将当前线程封装成一个Node节点,并将该节点添加到ConditionObject的等待队列

private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
} N
ode node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}

开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程,创建新的Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入条件队列尾部
在这里插入图片描述

3、fullyRelease(node)方法

接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁
释放锁,返回同步状态。释放锁失败的话,节点的waitStatus为CANCELLED

final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}

在这里插入图片描述
当ConditionObject队列加上线程到队尾后,又把同步器上的线程锁释放后,接下来就是去唤醒同步队列(阻塞队列)中睡眠的线程去竞争锁资源
即unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
在这里插入图片描述
在唤醒过后,还要把ConditionObject区域等待的线程阻塞住
在这里插入图片描述
总结:

  1. 调用addConditionWaiter()方法将当前线程封装成一个Node节点,并将该节点添加到ConditionObject的等待队列中
  2. 调用fullyRelease(node)方法将当前线程所持有的锁资源完全释放
  3. while (!isOnSyncQueue(node)){…},判断当前线程的节点是否在AQS的同步队列中,因为当前线程的节点刚刚被加入到ConditionObject的等待队列中,所以isOnSyncQueue(node)方法返回false,取反则会进入到while循环体中
  4. 调用LockSupport.park(this)方法,将当前线程阻塞,此时线程的状态为WAITING。此时线程等待被唤醒,唤醒后的线程会从LockSupport.park(this)方法开始继续向下执行。
  5. 当线程被唤醒后,执行语句if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)判断当
    前线程的唤醒方式,以及对应的后续操作。

有两种方法可以将一个状态为WAITING的线程唤醒

  • 调用LockSupport.unpark(Thread thread)方法。
  • 调用thread.interrupt方法。没错,调用中断方法会将一个线程唤醒

2.8 条件变量的single()原理

假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个Node,即 Thread-0 所在 Node。

1、single()方法
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}

在这里插入图片描述


2、doSignal(first)方法

将Condition的等待队列的头结点出队,并将符合条件的节点添加到AQS的等待队列中,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}

在这里插入图片描述


3、transferForSignal(Node node)方法

执行 transferForSignal 流程,将Thread-0 所在 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,
Thread-3 的 waitStatus 改为 -1

final boolean transferForSignal(Node node) {
//如果node节点的等待状态不为Node.CONDITION,则表示该节点的线程已经被取消或者在调用
signal()
//方法之前已经被其他线程中断,所以直接将该节点从condition的等待队列中剔除
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将当前节点添加到AQS的同步队列中,返回值p为当前节点在阻塞队列的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
  1. ws > 0 的情况是当前节点在AQS阻塞队列的前驱节点的状态为Node.CANCELLED,此时将会唤醒当前节点,并在acquireQueued方法中调用shouldParkAfterFailedAcquire方法将状态为
    Node.CANCELLED的节点从AQS的阻塞队列中剔除
  2. !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 为true,即当前线程准备通过CAS将前驱节点的状态改为Node.SIGNAL失败。从AQS获取锁和释放锁的代码中可以看出,AQS阻塞队列节点的前
    驱节点状态必须要为Node.SIGNAL。CAS修改失败可能可能的场景是在执行CAS的时候,前驱节点线程的状态变为Node.CANCELLED。
    在这里插入图片描述
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: ReentrantLock是Java中的一个锁类,它是一个可重入锁,允许同一个线程多次获得同一个锁。在使用ReentrantLock时,我们需要显式地获取锁和释放锁,可以通过lock()和unlock()方法来完成这些操作。 ReentrantLock采用了一种非公平的获取锁的方式,这意味着当多个线程同时请求锁时,ReentrantLock并不保证锁的获取顺序与请求锁的顺序相同。这种方式的好处是可以减少线程竞争,从而提高系统的并发性能。 另外,ReentrantLock还支持Condition条件变量,可以使用它来实现线程的等待和通知机制,以及更加灵活的线程同步和通信。 总之,ReentrantLock是Java中一个非常强大的锁类,可以帮助我们实现高效的线程同步和并发控制。但是,使用ReentrantLock也需要注意一些问题,比如需要正确地使用try-finally块来释放锁,避免死锁等问题。 ### 回答2: ReentrantLock是Java中的一种可重入锁,它提供了与synchronized关键字相似的功能,但具有更强大的扩展性和灵活性。 ReentrantLock内部使用一个同步器Sync来实现锁机制。Sync是ReentrantLock的核心组件,它有两个实现版本,分别是NonfairSync和FairSync。 NonfairSync是默认的实现版本,它采用非公平方式进行线程获取锁的竞争,即线程请求锁的时候,如果锁可用,则直接将锁分配给请求的线程,而不管其他线程是否在等待。 FairSync是公平版本,它按照线程请求锁的顺序来分配锁,当锁释放时,会优先分配给等待时间最长的线程。 ReentrantLock在实现上使用了Java的锁机制和条件变量来管理线程的等待与唤醒。当一个线程调用lock方法获取锁时,如果锁可用,线程会立即获得锁;如果锁被其他线程占用,调用线程就会被阻塞,进入等待队列。 当一个线程占用了锁之后,可以多次重复地调用lock方法,而不会引起死锁。这就是ReentrantLock的可重入性。每次重复调用lock都需要记住重入次数,每次成功释放锁时,重入次数减1,直到次数为0,锁才会被完全释放。 与synchronized相比,ReentrantLock提供了更多的高级功能。例如,可以选择公平或非公平版本的锁,可以实现tryLock方法来尝试获取锁而不会阻塞线程,可以使用lockInterruptibly方法允许线程在等待时可以被中断等等。 总之,ReentrantLock通过灵活的接口和可重入特性,提供了一种强大的同步机制,使多个线程可以安全地访问共享资源,并且具有更大的灵活性和扩展性。它在并发编程中的应用非常广泛。 ### 回答3: ReentrantLock是一种与synchronized关键字相似的线程同步工具。与synchronized相比,ReentrantLock提供了更灵活的锁操作,在并发环境中能更好地控制线程的互斥访问。 ReentrantLock原理主要包含以下几个方面: 1. 线程控制:ReentrantLock内部维护了一个线程的等待队列,每个线程通过调用lock()方法来竞争锁资源。当一个线程成功获取到锁资源时,其他线程会被阻塞在等待队列中,直到锁被释放。 2. 重入性:ReentrantLock允许同一个线程多次获取锁资源,而不会发生死锁。这种机制称为重入性。在线程第一次获取到锁资源后,锁的计数器会加1,当该线程再次获取锁时,计数器会再次加1。而在释放锁时,计数器会递减。只有当计数器减为0时,表示锁已完全释放。 3. 公平性和非公平性:ReentrantLock可以根据需要选择公平锁或非公平锁。在公平锁模式下,等待时间最久的线程会优先获取到锁资源。而在非公平锁模式下,锁资源会被直接分配给新到来的竞争线程,不考虑等待时间。 4. 条件变量:ReentrantLock提供了Condition接口,可以创建多个条件变量,用于对线程的等待和唤醒进行管理。与传统的wait()和notify()方法相比,Condition提供了更加灵活的等待和通知机制,可以更加精确地控制线程的流程。 总的来说,ReentrantLock是通过使用等待队列、重入性、公平性和非公平性、条件变量等机制,来实现线程的互斥访问和同步。它的灵活性和粒度更高,可以更好地适应各种复杂的并发场景。但由于使用ReentrantLock需要手动进行锁的获取和释放,使用不当可能会产生死锁等问题,因此在使用时需要仔细思考和设计。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值