前言
ReentrantLock基于aqs实现,而AQS里面维护了一条双向链表及当前node的状态,相对于synchronized来说,ReentrantLock的api更丰富,包含公平锁,非公平锁,条件阻塞等,今天我们就来讲解一下ReentrantLock锁的实现
ReentrantLock创建方式
/**
* 默认构造器,创建非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 由fair指定,true代表公平锁,false代表非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
看构造方法,创建这个对象的时候实际上是给sync创建对象,那么sync又是什么呢,下面是他的类图
看了类图,我们直到为什么ReentrantLock是基于AQS实现,它创建时其实是创建Sync的实现类,而Sync又是继承AbstractQueuedSynchronizer,我们简称为AQS
然后我们看他们的执行流程,然后再开始讲代码,我们以公平锁为例,下面是我自己画的流程图
公平锁上锁大致流程是这样,接下来根据源码进行讲解
首先我们先创建一个锁
Lock lock=new ReentrantLock();
lock.lock();
lock.unlock();
lock()
我们先进入lock.lock()操作,先看一下里面的操作,里面的方法就只有一个,我们选择FairSync实现类的方法,到acquire(1)方法
public final void acquire(int arg) {
//tryAcquire(arg) 尝试获取锁
if (!tryAcquire(arg) &&
//如果获取不到锁,则进入入队操作
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)尝试获取锁
然后我们进入tryAcquire(arg)方法,看如何尝试获取锁
/**
*尝试获取锁
*/
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取锁状态,0代表目前没有线程持有锁
int c = getState();
//如果当前没有线程持有锁
if (c == 0) {
//如果没有线程持有锁,进入判断,下面会讲到
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//如果cas成功,则将当前占有锁线程置为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//判断当前线程是不是已经占有锁
//如果已经占有锁,则代表重入锁,将state+1,代表占用次数计数
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
!hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//h != t 先判断这个队列中的头指针是否等于尾指针,如果等于,则代表当前还没被初始化,队列为null
return h != t &&
//1.(s = h.next) == null
// 判断头指针的下个指针是否为null
// 什么时候 h != t && (s = h.next) == null
// 进入初始化过程(enq(node)方法,下面会讲到),tail=head还没被执行,但是head已经被赋值
//2.s.thread != Thread.currentThread()
// 判断当前要占用锁的线程,队列中的下个节点的线程
// 其实时为了acquireQueued()之后tryAcquire(arg)准备
// 线程第一次进来,这个判断肯定为true,这里我们先记住这个操作,后面我会讲到
((s = h.next) == null || s.thread != Thread.currentThread());
}
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
acquireQueued()方法主要进行park和设置头节点和尝试占用锁操作,我们理解为出队操作
addWaiter()进行入队操作
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//判断队列为不为null,如果不为空则尝试进行入队操作
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//cas入队操作失败或者队列未初始化
enq(node);
return node;
}
enq(node)方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//如果尾节点为null,则进行cas初始化头节点操作,操作完之后
//还没进入到tail=head这个操作
//此时新来一个线程尝试获取锁
//对应上面 h != t && (s = h.next) == null这个判断
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//设置完头节点和尾节点之后,再次循环进来
//将当前线程进行入队操作,头节点代表占有锁线程
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
此时调用addWaiter()方法完毕,返回当前线程的节点信息,然后调用acquireQueued(final Node node, int arg)方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//流程图的死循环2
for (;;) {
//node.predecessor();获取node的上个节点
final Node p = node.predecessor();
//如果上个节点为头节点
//那么尝试获取一下锁
//这里的尝试获取锁,调用上面tryAcquire(arg)的hasQueuedPredecessors()方法
//此时的这个判断 s.thread != Thread.currentThread()此时的这个判断是为false
//代表即将要获取锁的线程和当前相乘相等,并且在外面的判断是!hasQueuedPredecessors()
//所以进入获取锁操作
//获取锁结束完,设置头节点为当前节点
//然后setHead()方法中会将弄得的线程设置为null,然后prev设置为null
//再将之前头节点next指针设置为null,方法jvm gc,返回中断状态
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire 判断p.waitStatus状态是否是阻塞状态
//0 将waitStatus改为-1,循环一次
//然后判断是否为负数,如果是,则返回true
//这两个方法下面简单描述一下
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞线程操作,返回线程是否被中断
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(p, node)方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//获取上个节点的等待状态,如果是-1,则进行阻塞
return true;
if (ws > 0) {
//上个节点如果是取消状态,则将上个节点移除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//获取上个节点的等待状态,如果是-1,则放回false,再让外面的方法进行一次循环
//个人认为这里是比较体现乐观锁思想的一个地方
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()方法
private final boolean parkAndCheckInterrupt() {
//阻塞线程
LockSupport.park(this);
//返回线程是否被中断,在其他地方调用该方法的时候,会抛出异常,例如doAcquireInterruptibly()
//然后重置中断状态
return Thread.interrupted();
}
由此,线程阻塞
- 调用LockSupport.unpark(Thread );方法或者interrupt();方法,线程取消阻塞
- 然后再次循环,尝试获取锁,当获取到锁时,整个方法返回
- 如果acquire(1)中的判断为true,说明线程是被中断阻塞的,所以要将中断标记重新打上
- 如果为false,说明线程是通过LockSupport.unpark(Thread)解除阻塞,直接返回即可
- 到此,线程获取锁成功,这就是ReentrantLock获取锁流程
上锁总结
其实Lock锁都会一次次的去循环尝试获取锁,总是乐观的认为当前线程能获取到锁,直到最后阻塞,这就是乐观锁的思想
unlock()方法解锁
unlock方法相对加锁流程更容易理解
//unlock调用该方法释放锁
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
Node h = head;
//头指针不为空,代表有队列
//头指针的等待状态不等于0,代表下个节点被阻塞
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease方法
protected final boolean tryRelease(int releases) {
//state-1 ,直到c=0
//如果c!=0,代表重入锁,当前线程继续占有锁
int c = getState() - releases;
//释放锁的时候,必须确定该线程已经被加锁,不然会报异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//c==0,则将占有锁的线程释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor()方法
//尝试释放锁成功,调用该方法
private void unparkSuccessor(Node node) {
//获取当前等待状态
int ws = node.waitStatus;
if (ws < 0)
//<0将等待状态通过cas置为0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//判断下个节点是否是取消状态,如果是取消状态,则将该节点移除
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;
}
//如果头指针的下个节点不等于null,则将该线程unpark,取消阻塞
if (s != null)
LockSupport.unpark(s.thread);
}
到此,线程解锁成功
讲了公平锁,非公平锁大家感兴趣可以去看一下源码,其实流程差不多,也是用双向队列来维护阻塞的线程,但是当头指针释放锁的时候,新进来的线程会和队列中线程竞争这把锁,来达到非公平的效果
最后,如果上述代码或流程图有错的地方,请大家评论留言