谈到Java的锁机制就会想到Synchronized关键词、ReentrantLock锁,谈到ReentrantLock锁不得不说AQS框架,前面的文章已经将AQS框架的源码原理给大家讲解了一下,今天就讲讲ReentrantLock锁。
如果不熟悉AQS框架强烈建议看看我前面这篇博文https://blog.csdn.net/qq_37892957/article/details/88283164
ReentrantLock是可重入的独占锁,为什么ReentrantLock是可重入的呢,因为ReentrantLock底层是使用来一个内部类即(Sync同步器)来实现AQS框架,在AQS源码中我们看到AQS框架集成了一个抽象类AbstractOwnableSynchronizer,该抽象类中有一个变量exclusiveOwnerThread,这个变量的作用就是用来表示当前持有锁的线程,重入锁的概念reentrantLock.lock是可以嵌套几次使用的,在这个嵌套的过程中我们得明白Java类如何知道当前线程是否拥有了锁,所以说这个变量就是能够用来判断当前线程是否是一个持有锁的独占线程。
前面讲完ReentrantLock的基本原理思想,下面我们介绍一下ReentrantLock中公平锁和非公平锁。
公平锁的概念就是假设有十个人在食堂打饭,如果每个人都抢着打饭肯定会很乱,为了规范这种制度提出了排队打饭的思想,打饭就是获取到锁,排队就是公平锁能够按照次序依次让每个人都能够打上饭。
下面看看ReentrantLock是如何实现公平锁的
/**
* ReentrantLock内部维护的内部类 FairSync公平锁机制
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//开始竞争 底层调用的是AQS框架的acquire方法 这里不多讲解了
final void lock() {
acquire(1);
}
/**
* 重写AQS框架的tryAcquire方法 因为在AQS框架中tryAcquire方法并未写出具体实现的代码,都是需要其继承类按需重写
*
* @param acquires
* @return
*/
protected final boolean tryAcquire(int acquires) {
//拿到当前线程
final Thread current = Thread.currentThread();
//获取锁状态
int c = getState();
//如果状态为0 说明并未有线程来竞争锁
if (c == 0) {
//再次判断是否存在等待队列中有其他线程在等待锁 如果不在阻塞队列中则尝试CAS替换锁状态,如果替换成功则说明拿到了锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//拿到了锁返回
setExclusiveOwnerThread(current);
return true;
}
//否则就来判断当前线程是否是重入拿锁的 如果是则锁状态继续加1
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果又没拿到锁 又不是重入锁则直接返回false表示我没有拿到锁
return false;
}
}
公平锁的核心代码如上所示,下面我们介绍非公平锁。
什么是非公平锁呢,还是食堂打饭的原理,如果有很多人挤进了食堂,这个学校并没有推出排队打饭的道理,我们只能凭本事去打饭,谁打到饭就是老大,我打到饭的同时我可能还是帮别人带几份饭,这样可能会造成食堂饭不够吃的情况。饭代表这锁,帮别人带饭代表这可重入锁,不排队凭本事打饭竞争锁,饭不够造成后面实力弱的学生吃不到饭说明会造成饥饿的情况即线程饥渴,线程饥渴表示前面一直都有本事拿到饭,我实力差抢不到打饭的锁就是这样一个道理。
/**
* 不公平竞争锁
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 不公平锁竞争核心方法 直接就判断我是不是能够拿到锁状态 如果能够成功修改则我拿到了 如果没有则把我放进阻塞队列里面我继续去竞争锁
*/
final void lock() {
//不公平锁竞争方式 直接CAS设置state状态是否为等待获取锁的方式 如果为0代表是 否则已经有线程获取到了锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//已经有线程获取到锁 尝试使用AQS获取锁
acquire(1);
}
//试图去竞争锁
protected final boolean tryAcquire(int acquires) {
//调用内部方法
return nonfairTryAcquire(acquires);
}
}
/**
* 不公平锁获取的方式
*
* @param acquires
* @return
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取线程的状态
int c = getState();
//如果线程状态为0 说明是正在获取同步状态 当前线程可以获取到锁
if (c == 0) {
//这里和公平锁不同的是 不需要判断是否是阻塞队列中 因为不公平锁只要没被取消都有机会能够竞争到锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//判断如果当前线程已经获取到独占锁了
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
总结:
1、非公平锁在使用lock()方法上,直接就是CAS修改锁,如果能修改则我拿到锁如果修改不了则我继续等待并尝试获取锁。
2、公平锁在使用lock()方法上,会判断等待队列中是否存在其他线程抢锁的情况,如果存在我则排到等待队列的最后再慢慢等前面的线程执行完毕到我。即就是排队打饭得等前面打完饭我才能打到饭。
相对来说,非公平锁会有很好的性能,因为它的吞吐量大,但是非公平锁容易让那些获取不到锁的线程无限等待,导致阻塞队列长期处于饥渴状态。
上面说完ReentrantLock的公平锁和非公平锁,下面我介绍一下ReentrantLock重入锁的好搭档中的Conditiion,Condition经常被用来使用到生产者-消费者模式。
在ReentrantLock中又是如何使用condition的呢
//直接调用的就是同步器的newCondition
//直接调用的就是同步器的newCondition
public Condition newCondition() {
return sync.newCondition();
}
我们追踪一下同步器的newCondition()方法。
final ConditionObject newCondition() {
return new ConditionObject();
}
我们看到Sync同步器内部创建了一个new ConditionObject()接下来我们再看看什么是ConditionObject
点进去发现又回到了AQS框架里面了
我们看见该类实现一个Condition的接口 我们跳到这个接口中看看这接口定义了一些什么
我们可以看到接口中定义了这些方法,这些方法到底有什么含义呢?其实和我们用的wait()让线程等待、notify()方法类似 ,如果前面我的那篇博文看过的话应该看起来不难,现在我说明一下这几个方法中最具代表性质的方法。
await()方法会使当前线程等待,同时释放当前线程占有的锁,直到其他线程调用signal()或者signalAll()方法来唤醒这个等待的线程,线程便能够重新获得锁并继续执行。当线程在等待的过程中被中断也能跳出等待。是不是和Object.wait()方法很相似呢。
signal()方法调用之后系统会在Condition的等待队列中唤醒一个线程。一旦线程被唤醒就能够重新获得锁并继续执行。是不是感觉和Object.notify()方法类似呢。
接下来介绍完这condition接口,我们来介绍一下AQS框架中的阻塞队列和等待队列是什么
前文AQS框架的介绍中,我们说到条件地理和阻塞队列都是AQS框架中内部类Node节点的实例,而条件队列如果调用condition.signal()方法唤醒则需要重新回到阻塞队列的尾部来重新获取锁执行,每当有一个调用awit()方法则会携带node节点进入condition的等待队列中。每当condition的条件队列中有一个线程被唤醒则出队列让后面一个等待线程变成头节点。
我们回顾一下前文Node节点中是不是有个参数并未给大家介绍就是
这个内部类的变量nextWaiter维护的就是等待队列的单链表结构。
接下来让我们看看Condition接口中定义的 awit在ConditionObject中是如何实现的
/**
* 如果当前线程被中断,则抛出InterruptedException。
* 保存{@link #getState}返回的锁定状态。
* 以保存状态作为参数调用{@link #release},如果失败则抛出IllegalMonitorStateException。
* 阻止直至发出信号或中断。
* 通过调用专用版本重新获取
* {@link #acquire} 以保存的状态作为参数。
* 如果在步骤4中被阻止时被中断,则抛出InterruptedException。
*/
public final void await() throws InterruptedException {
//检查节点是否在等待过程中中断过
if (Thread.interrupted())
throw new InterruptedException();
//这里将调用的方法的线程放入等待队列的尾部 并返回这个等待节点
Node node = addConditionWaiter();
//这里我们应该想到先头说的调用awit方法需要释放当期锁占用的共享资源
//这里拿到的就是未释放前的锁状态值
int savedState = fullyRelease(node);
//判断在后续过程中是否当前线程被其他线程所中断
int interruptMode = 0;
//判断线程是否在等待队列还是阻塞队列中
while (!isOnSyncQueue(node)) {
//阻塞线程
LockSupport.park(this);
//检查节点是否被中断 如果被中断则跳出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//REINTERRUPT: 代表 await 返回的时候,需要重新设置中断状态
//THROW_IE: 代表 await 返回的时候,需要抛出 InterruptedException 异常
//0 :说明在 await 期间,没有发生中断
//判断节点是否在前面的过程中走到了头节点 并且当前等待节点是否被其他线程给唤醒了
//然后检查线程是否被中断过了
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT; //REINTERRUPT 说明线程获取到了锁并没有被中断
//如果在执行过程中节点被取消 则将等待队列中被取消的节点清除
if (node.nextWaiter != null)
//释放空节点
unlinkCancelledWaiters();
//如果线程不为未中断状态
if (interruptMode != 0)
//获取到唤醒信号或者说中断信号
reportInterruptAfterWait(interruptMode);
}
接下来我们看看第一个调用将这个线程添加进等待队列末尾的方法addConditionWaiter()
//新增节点进入等待队列中
private Node addConditionWaiter() {
//指向等待队列的尾节点
Node t = lastWaiter;
//如果尾节点不为空说明尾节点存在 再判断尾节点的元素是否是等待状态 如果不是等待状态说明节点在等待队列中被取消了 则清除等待队列中的所有被取消的节点
//等待队列的节点存的waitStatus就是Node.CONDITION 所以采用这种方法来判断
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//以当前执行线程创建一个等待队列 因为如果是一个线程调用awit()方法必须是获取到了执行的锁才生效
// 所以这里我们直接创建节点并放入等待队列中
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//如果尾节点为空 说明可能可能
if (t == null)
//则将尾节点设置为刚创建的节点 这样节点链执行就生效了
firstWaiter = node;
else
//否则就将尾节点的下一个等待节点指向新创的节点
t.nextWaiter = node;
//然后再将尾节指向新创建的节点
lastWaiter = node;
//返回当前线程在等待队列中创建的节点
return node;
}
然后开始释放当前线程执行的共享资源
/**
* 使用当前状态值调用release;返回保存状态。 取消节点并在失败时抛出异常。
*
* @param node the condition node for this wait
* @return 返回保存状态
*/
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;
}
}
然后都没问题执行到获取线程是否存在于阻塞队列中即是否是获取同步状态了
/**
* 先前的图说了 如果当前节点被其他线程唤醒需要放入阻塞队列中 这里我们判断当前的节点是否是在等待队列中或者说是获取同步的状态
*
* @param node the node
* @return true if is reacquiring
*/
final boolean isOnSyncQueue(Node node) {
//判断节点的状态是否是等待状态 或者节点的头节点不为空
//这里为什么判断节点头结点不为空呢?因为等待队列的头结点如果他提前进入了阻塞队列中
// 需要将下一个节点来设置成头节点并等待其他线程优先唤醒
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//如果下一个节点不为空 则必然在等待队列中 如果它为尾节点则进行下一步判断
if (node.next != null) // If has successor, it must be on queue
return true;
/*
* node.prev可以是非空的,但尚未在队列中,因为将CAS放在队列中的CAS可能会失败。所以我们必须从尾部进行遍历,
* 以确保它实际上成功。在调用这种方法时,总是接近尾部,除非CAS失败(这不太可能),
* 它将在那里,所以我们几乎不会遍历很多。
*/
//继续判断节点是否是在等待队列中
return findNodeFromTail(node);
}
如果羡慕的节点判断都不能证明节点是在等待队列中,则我们继续判断节点是否在等待队列的尾节点中
private boolean findNodeFromTail(Node node) {
//拿到当前等待队列的尾节点
Node t = tail;
//无限循环
for (; ; ) {
//如果传入的节点就是尾节点则直接返回true 说明就是在等待队列中
if (t == node)
return true;
//如果尾节点为空说明节点
if (t == null)
return false;
//如果尾节点又不是空 又不是当前执行节点 则继续向前遍历
t = t.prev;
}
}
如果执行的过程中发现节点的状态被设置为不等于Node.CONDITION时候说明节点被其他线程取消了,则调用unlinkCancelledWaiters()释放等待队列中的垃圾节点即取消的的节点。
/**
* 释放那些等待队列中被取消的节点即垃圾节点
*/
private void unlinkCancelledWaiters() {
//从头节点开始遍历
Node t = firstWaiter;
Node trail = null;
//如果头结点不为空 说明等待队列中还有元素 开始执行
while (t != null) {
//拿到头节点的下一个节点
Node next = t.nextWaiter;
//如果节点等待状态不为condition 则说明是垃圾节点被取消了的
if (t.waitStatus != Node.CONDITION) {
//让这个节点设置为空 方便GC回收空节点
t.nextWaiter = null;
//如果trail是第一次进来
if (trail == null)
//则让节点等于下一个节点
firstWaiter = next;
else
//否则说明是循环几次之后了 则直接拿到节点的下一个节点
// 即后面如果还有节点则将后面那个空的节点剔除前先建立链表结构 始终让这个节点能够指向下一个节点
trail.nextWaiter = next;
//如果下一个节点为空
if (next == null)
//则说明节点已经执行到了末尾了 循环能够跳出了
lastWaiter = trail;
} else
//如果下一个节点不为垃圾节点 则直接赋值给trail继续判断等待队列中是否存在垃圾节点
trail = t;
//直接t.next 继续循环
t = next;
}
}
然后继续往下执行发现线程如果不为中断状态说明线程可能获取到了唤醒信号或者说是被中断了
/**
* 抛出InterruptedException,重新中断当前线程,或不执行任何操作,具体取决于模式。
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
//如果线程被中断则抛出中断异常
if (interruptMode == THROW_IE)
throw new InterruptedException();
//获取说线程
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
到此为止await()方法已经执行完毕了,具体的注释我都已经在上面代码上面已经说明,可以有序的往下看 再返回去浏览
下面开始介绍signal() condition等待队列的唤醒方法
/**
* 将等待队列中的头节点唤醒并放入阻塞队列中让其获取同步状态
*
* @throws IllegalMonitorStateException if {@link #isHeldExclusively}
* returns {@code false}
*/
public final void signal() {
//判断当前线程是否独占锁 即是否获取到了锁 如果没有获取到锁则抛出异常
//因为signal和await方法都是要获取到锁才能执行的
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//指向等待队列的头节点
Node first = firstWaiter;
//如果头节点不为空 说明等待队列中存在元素 此时可以执行唤醒线程的操作
if (first != null)
doSignal(first);
}
如果头节点不为空 说明等待队列中存在元素能够被唤醒 我们执行唤醒元素的操作。
/**
* 唤醒头节点的操作
*
* @param first (non-null) the first node on condition queue
*/
private void doSignal(Node first) {
do {
//如果头节点的下一个为空 说明等待队列就一个元素了
if ((firstWaiter = first.nextWaiter) == null)
//尾节点也置换为空
lastWaiter = null;
//并将传入的头节点下一个节点设置为空
first.nextWaiter = null;
//如果节点被取消或已经处于唤醒状态了 则继续等待队列头结点是否为空 如果不为空则继续执行
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
接着我们看看循环条件的方法 该方法的作用就是将节点传入阻塞队列中
/**
* 将节点从条件队列传输到阻塞队列。 如果成功则返回true。
*
* @param node the node
* @return true if successfully transferred (else the node was
* cancelled before signal)
*/
final boolean transferForSignal(Node node) {
//CAS替换节点的状态为0 说明节点能够获取等待同步的状态 如果替换不了 说明节点被取消了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//如果前面的方法替换成功了 说明节点已经能够进入阻塞队列中了 此时将节点放入阻塞队列中
Node p = enq(node);
//获取节点的状态
int ws = p.waitStatus;
//如果节点的状态>0说明节点被取消或者节点替换成能够唤醒状态失败
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//成功则将等待队列的这个节点取消等待阻塞状态
LockSupport.unpark(node.thread);
//返回true
return true;
}
文章参考:
https://javadoop.com/post/AbstractQueuedSynchronizer-2可以多去了解一下这个大佬的文章 写得非常经典 很有学习借鉴的意义,不过看永远只是看,理解并学以致用才是王道。
-- 不是非得赢 我只是不想输。