一、ReentrantLock概念
ReentrantLock是JAVA并发情况下提供的用来加锁的机制,位于JUC包下,提供了一系列的加锁释放锁的方法,使用起来非常简单,只需要在代码块之前调用lock()方法,在finally中调用unlock()方法即可解决并发的问题。
1.1、AQS
AQS实际上是AbstractQueuedSynchronizer的缩写,是JAVA提供的用来实现锁的一个抽象类,它有一个基类AbstractOwnableSynchronizer,可以理解为它为实现ReentrantLock等其他锁提供了一个基础的容器,这些容器在不同的锁的实现中都存放着实现这些锁所需要的基本组件。
- 1、Node:AQS中的内部类,代表着一个节点,内部有一个Thread线程,每一个线程竞争锁的时候,都会被封装成一个Node放在队列中排队。
- 2、exclusiveOwnerThread:这是AQS的基类中的变量,用来表示当前已经竞争到锁的线程,
- 3、state:AQS中的变量,是一个volatile int类型的变量,线程能不能竞争到锁,就看线程能不能通过CAS的方式将该变量从0改成1。
- 4、head:AQS中的变量,是一个volatile Node类型的变量,用来标识头结点。
- 5、tail:AQS中的变量,是一个volatile Node类型的变量,用来标识尾节点。
1.2、CAS
CAS相关知识,参考本人博客:JAVA CAS问题原理
二、ReentrantLock源码流程
2.1、lock():
实际上会调用sysn的lock(),而sync在ReentrantLock中有两种实现,也就是FairSync(公平锁)和NonfairSync(非公平锁),默认为NonfairSync。
java
public void lock() {
// 默认调用NonfairSync中的lock()
sync.lock();
}
2.2、NonfairSync中的lock()
java
final void lock() {
// 因为是非公平锁,一上来就先竞争锁,
if (compareAndSetState(0, 1))
// 如果获取到锁,就将exclusiveOwnerThread变量设置为自己,表明当前是自己获取到锁了。
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果没有获取到锁,那么就开始排队
acquire(1);
}
2.3、acquire()方法是AQS中提供的方法
java
public final void acquire(int arg) {
// 再次尝试获取锁,如果获取锁失败那么就将当前线程封装成Node节点开始排队
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 并且将当前线程设置为停止状态,也就是说,放弃竞争了。
selfInterrupt();
}
2.4、addWaiter()方法是AQS中提供的方法
java
private Node addWaiter(Node mode) {
// 首先将当前线程封装成一个Node节点
Node node = new Node(Thread.currentThread(), mode);
// 存储头结点
Node pred = tail;
// 如果头结点不为null,
if (pred != null) {
// 当前节点的前一个节点指向tail
node.prev = pred;
// 并且通过CAS的方式将当前节点和tail进行交换
if (compareAndSetTail(pred, node)) {
// 如果当前节点和tail进行交换成功,头结点的下一个节点指向当前节点,并返回当前节点
pred.next = node;
return node;
}
}
// 如果头节点为空,说明当前节点是第一个节点,队列中没有其他节点
enq(node);
return node;
}
2.5、enq()方法是AQS中提供的方法
java
private Node enq(final Node node) {
for (;;) {
// 将头节点进行缓存
Node t = tail;
// 如果头结点为null,说明此时队列中没有其他节点,只有当前节点一个
if (t == null) {
// 创建一个新的节点,该节点没有任何意义,只是AQS实现上为了帮助后续唤醒线程而设计
if (compareAndSetHead(new Node()))
// 头尾指向同一个节点
tail = head;
} else {
// 如果头结点不为null,当前节点的前一个节点指向tail
node.prev = t;
// 通过CAS方式设置当前节点为头节点
if (compareAndSetTail(t, node)) {
// 头结点的下一个节点指向当前节点
t.next = node;
return t;
}
}
}
}
2.6、acquireQueued()方法是AQS中提供的方法
java
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);
// 将当前节点的前一个节点(实际上就是头结点)设置为null,
p.next = null; // help GC
failed = false;
// 只有在竞争不到锁的情况下,才是true,表明停止当前线程,放弃竞争锁
return interrupted;
}
// 当线程竞争锁失败后,是不是需要暂停线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 当线程竞争锁失败后,需要取消竞争
cancelAcquire(node);
}
}
2.7、nonfairTryAcquire()是Sync中提供的竞争锁的方法。
java
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)) {
// 如果成功,就获取到锁,将当前线程设置为自己并返回true,表明加锁成功。
setExclusiveOwnerThread(current);
return true;
}
} // 如果state的值不为0,那么就是判断当前线程是不是自身线程,如果是,说明本身已经获取到锁了,就不在需要重复竞争锁了,实际上就是可重入锁的判断
else if (current == getExclusiveOwnerThread()) {
// 可重入次数的判断,实际上ReentrantLock中不可能出现这种情况,这里知识容错处理
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 将state的值设置为重入的次数,并返回true,表明加锁成功。
setState(nextc);
return true;
}
// 否则其他情况都是加锁失败的情况
return false;
}
三、ReentrantLock总结
1、ReentrantLock可以实现公平锁和非公平锁以及可重入锁的机制,基于AQS和CAS进行加锁处理, 提供了lock和unlock两个api方法进行加锁和释放锁处理。 2、以非公平锁为例,lock方法一进去就竞争锁,通过CAS的方式将state变量从0设置为1,如果设置成功,表明竞争锁成功,就将线程变量设置为当前线程,如果失败,就开始排队。 3、再次尝试获取锁(tryAcquire),如果继续失败,那么就放弃竞争锁,将自己状态设置为interrupted的。并且开始排队。 4、排队的时候,将自身线程封装成一个Node节点,找到一个不失效的前继节点,当前节点的pred指向前继节点,前继节点的next指向当前节点。 4、可重入的判断,实际上,如果发现当前state不为0,那就说明有线程正在占用资源,那么只需要判断current == getExclusiveOwnerThread()是否成立即可,如果成立就是可重入的,获得锁,如果不成立,就说明,当前占用资源的线程不是自身,获取锁失败。