reentranLock介绍
-
java中提供的锁:synchronized、lock锁
-
reenatranLock就是一个互斥锁,可以让多线程执行期间,只有一个线程在执行指定的一段代码
-
使用方式
java
public static void main(String[] args) {
ReentrantLock lock=new ReentrantLock();
try{
lock.lock();
//执行业务逻辑
}finally {
lock.unlock();
}
}
reentranLock的lock方法源码
在进入到lock方法后,发现内部调用了sync.lock()方法,去找方法的实现时,发现了两个实现
- FairSync:公平锁 每个线程都会在执行lock方法时,会先查看是否有线程排队,如果有,直接去排队。如果没有才会去尝争一下锁资源。
- NonfairSync: 非公平锁 每个线程都会在执行lock方法时,先尝试获取锁资源,获取不到再排队。
如果需要使用公平锁:在new ReentrantLock时,传入参数true
如果需要使用非公平锁:直接无参构造
更加推荐非公平锁,非公平锁效率比公平锁高
java
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
非公平lock方法源码
java
//非公平锁的lock方法
final void lock() {
//以CAS的方式,尝试的state从0改为1
if (compareAndSetState(0, 1))
//证明修改state成功,也就是代码获取锁成功
//将当前线程社设置到exclusiveOwnerThread(AQS中),代表当前线程拿着锁资源(和后面的可重入锁有关系
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
公平锁的lock源码
java
final void lock() {
acquire(1);
}
分析AQS
- AQS就是AbstractQueuedSynchronized类,AQS内部维护着一个队列,还有三个核心属:state、head 、tail
- 线程在竞争ReentrantLock的锁资源时,只要通过CAS将state从0设置为1,即代表竞争锁资源成功
reentranLock的acquire方法源码
java
//公平锁还是非公平锁都会调用当前的acquire方法
public final void acquire(int arg) {
//tryAcquire方法,分为两种实现。第一种是公平锁,第二种是肥公平锁
//公平锁操作:如果state为0,再看看是否有线程排队,如果有我就去排队。如果是锁重入的操作,直接获取锁
//非公平锁操作:如果state为0,直接尝试CAS修改,如果是锁重入的操作,直接获取锁
if (!tryAcquire(arg) &&
//addWaiter 方法:在线程没有通过tryAcquire拿到锁资源时,需要将当前线程封装为node对象,去AQS内部排队。
//acquireQueued方法:查看当前线程是否排在队伍前面,如果是就尝试获取锁资源。如果长时间没拿到锁,也要将当前线程挂起
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
reentranLock的tryAcquire方法源码
tryAcquire方法是AQS提供的,内部并没有任何实现,需要继承AQS的类自己去实现逻辑代码
查看到tryAcquire在ReentrantLock中提供了两种实现:公平锁、非公平锁
java
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
非公平锁实现
java
//非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
//拿到当前线程
final Thread current = Thread.currentThread();
//拿到AQS的state值
int c = getState();
//如果state为0,这就代表当前没有线程占用资源
if (c == 0) {
//直接基于CAS的方式,尝试修改state,从0改为1,如果成功就代表拿到锁资源
if (compareAndSetState(0, acquires)) {
//将exclusiveOwnerThread属性设置为当前线程
setExclusiveOwnerThread(current);
//返回true
return true;
}
}
//说明state肯定不为0,不为0就代表当前lock被线程占用
//判断占用资源的线程是不是当前线程
else if (current == getExclusiveOwnerThread()) {
//锁重入操作 对state+1
int nextc = c + acquires;
//判断锁重入是否已经达到最大值
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//将AQS的state设置好
setState(nextc);
//返回true
return true;
}
return false;
}
公平锁实现
java
//公平锁实现
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//过去state
int c = getState();
//没有线程占用锁资源
if (c == 0) {
//首先查看,有没有线程排队
if (!hasQueuedPredecessors() &&
//如果没有线程排队,CAS尝试获取锁资源
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");
setState(nextc);
return true;
}
return false;
}
tryAcquire公平锁与非公平锁区别
tryAcquire方法公平锁与非公平锁区别就是,当判断state为0之后
- 公平锁会先查看是否有线程正在排队,如果有返回false,如果没有线程排队,执行CAS尝试获取锁资源
- 非公平锁不管有没有线程排队,直接以CAS的方式尝试获取锁资源
reentranLock的addWaiter方法源码
在线程执行tryAcquire方法没有获取到锁资源之后,会返回false,再配置上if中的!操作,会执行&&后面的方法,而在acquireQueued(addWaiter(Node.EXCLUSIVE),arg)的参数中执行了addWaiter,要将当前获取锁失败的线程封装为Node,排队到AQS的队列中。
java
//获取锁失败,封装node,排队到AQS的队列中
private Node addWaiter(Node mode) {
//将线程封装为node对象
Node node = new Node(Thread.currentThread(), mode);
// 获取到tail节点,prev
Node pred = tail;
//如果tail节点不为null
if (pred != null) {
//将当前节点的prev,指向tail
node.prev = pred;
//为了避免并发问题,以CAS的方式将tail指向当前线程
if (compareAndSetTail(pred, node)) {
//将之前的tail的tailnext,指向当前节点
pred.next = node;
//返回当前节点
return node;
}
}
enq(node);
return node;
}
整体逻辑为,先初始化Node节点,将当前线程传入,并且标识为互斥锁。 尝试将当前Node插入到AQS队列的末尾
- 队列为空:执行enq,先初始化空Node作为头,然后再将当前Node插入,作为tail
- 队列不为空:直接将当前Node插入,作为tail
reentranLock的acquireQueued方法源码
首先查看当前node是否排在队列的第一个位置(不算head,因为第一个节点是 head),是的话尝试获取锁资源直接再次执行tryAcquire方法竞争锁资源; 不是的话尝试将当前线程挂起(为什么是尝试,主要在挂起的时候要保证前一个节点是正常的,可能是排的时间太长了后面就不排了,但是node没有移除掉,是取消状态),最终排在有效节点后才会将当前线程挂起
1、以下代码中如果当前node为head的下一个,直接尝试获取锁资源
java
//如果是队列前面,竞争锁资源,非队列前面,挂起线程
final boolean acquireQueued(final Node node, int arg) {
//竞争锁资源失败
boolean failed = true;
try {
//线程中断标识
boolean interrupted = false;
//死循环
for (;;) {
//predecessor就是获取当前节点的上一个节点
final Node p = node.predecessor();
//如果当前节点的上一个节点是head,如果是head后面的节点,就执行tryAcquire方法,竞争资源
if (p == head && tryAcquire(arg)) {
//竞争资源成功,进入当前业务代码
//因为当前线程已经拿到锁资源,将当前线程的node设置为head,并且将node中 prev和head致为null
setHead(node);
//将之前的头节点的next置为null,让GC将之前的head回收
p.next = null; // help GC
//将获取锁失败的标识置为false
failed = false;
//返回线程中断标识,默认情况为false
return interrupted;
}
//见下方说明
if (shouldParkAfterFailedAcquire(p, node) &&
//见下方说明
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2、尝试将当前线程挂起,涉及到了判断以及LockSupport方法
java
//判断当前线程是否可以挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//拿到了上一个节点的状态
int ws = pred.waitStatus;
// 如果ws为-1(Node.SIGNAL=-1),直接返回true,当前节点可以挂起,
if (ws == Node.SIGNAL)
return true;
//如果ws>0,说明肯定是cancelled状态(只有cancelled=1),绕过这个节点,找上一个节点的上一个
if (ws > 0) {
//循环,直到找到上一个节点为小于等于0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//可能为0 -2 -3 ,直接以cas的方式将节点状态改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//找到上一个节点状态是正常的后,就可以调用当前方法将线程挂起
private final boolean parkAndCheckInterrupt() {
//直接使用Unsade类的part方法挂起线程
LockSupport.park(this);
return Thread.interrupted();
}
reentranLock的unlock方法源码
-
unlock释放锁操作不为公平锁和非公平锁,都是执行sync的release方法
-
释放锁的核心,就是将state从大于0的数值更改为0即为释放锁成功
-
并且unlock方法应该会涉及到将AQS队列中阻塞的线程进行唤醒,阻塞用的park方法,唤醒必然是unpart方法
csharp
public void unlock() {
//每次只释放1
sync.release(1);
}
reentranLock的release方法源码
不分公平与非公平
- 在释放锁时,是有state被减为0之后,才会去唤醒AQS队列中被挂起的线程
- 在唤醒挂起线程时,如果head的next状态不正确,会从后往前找到离head最近的节点进行唤醒
- 为什么从后去、往前找?(addWaiter的是先将prev指针赋值,最后才会将上一个节点的next指针赋值,为了避免丢失节点或者跳过节点,必须从后往前找)
java
// 释放锁操作 其中arg为1
public final boolean release(int arg) {
//先查看tryRelease方法,具体见下方
if (tryRelease(arg)) {
//释放锁成功,进行后续处理
Node h = head;
//如果head不为null,并且当前的head的状态不为0=》只有状态为-1的时候后面才有一个线程处理挂起,为0代表后面没有线程挂起
if (h != null && h.waitStatus != 0)
//说明AQS的队列中,有node 在排队,并且线程已经挂起了,具体见下方方法说明
unparkSuccessor(h);
return true;
}
//返回false,代表释放一次没有完全释放掉
return false;
}
//优先查看的tryRelease 参数releases=1,因为是上方传来的
protected final boolean tryRelease(int releases) {
//直接获取state,并且-releases=》将state-1
int c = getState() - releases;
//如果释放锁的线程,不是占用锁的线程,直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//声明一个标识
boolean free = false;
//判断state-1后是否为0
if (c == 0) {
//如果为0,锁资源释放掉
free = true;
//架构占用互斥锁的线程标识置为null
setExclusiveOwnerThread(null);
}
//锁之前重入了,一次内释放掉,将C赋值给state,等待下次再次执行是,再次判断
setState(c);
return free;
}
//唤醒AQS中被挂起的线程
private void unparkSuccessor(Node node) {
//获取head的状态
int ws = node.waitStatus;
if (ws < 0)
//将当前的head状态设置为0
compareAndSetWaitStatus(node, ws, 0)
//拿到next节点
Node s = node.next;
//如果下一个节点为null,或者状态为cancel,需要找到离head节点最近的有效node大于0只有是cancelled状态
if (s == null || s.waitStatus > 0) {
s = null;
//从后往前找这个节点(为什么从后往前找,需要查看addWaiter的内容)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//找到最近的node后,直接唤醒
if (s != null)
//唤醒线程
LockSupport.unpark(s.thread);
}