ReentrantLock是一个可重入的锁,内部采用AQS来实现
下面我会对其源码进行详细的分析,因为贴了源码,所以我会直接在源码上进行注释,并加入一些我自己的理解,以求大家可以更容易的理解。废话不多说,直接开始:
ReentrantLock的构造函数:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可以看到构造函数做了一件事,就是将lock实例的sync字段进行了初始化,从代码可以看出根据参数的不同,将sync初始化为不同的实现,即公平锁FairSync和非公平锁NonfairSync,那sync到底是什么呢?
abstract static class Sync extends AbstractQueuedSynchronizer {}
具体的代码就不粘出来了,可以看到Sync继承自AbstractQueuedSynchronizer抽象队列同步器AQS,也就是说ReentrantLock的真正实现其实是依赖了AQS,下面就对AQS进行分析
首先看一下AQS中比较重要的属性
exclusiveOwnerThread:独占锁线程,指向了当前获取到锁的线程
state:AQS的核心,AQS就是用这个字段来实现锁的获取和重入,在没有线程获取到锁的时候,锁的状态为0,获取的时候,通过cas对其进行+1,并且每重入一次再 +1,释放一次 -1,具体的后面代码展示
head,tail:AQS维护了一个内部类Node的双向队列,由未获取到锁的线程包装成的Node节点组成,也就是获取锁失败加入队列尾部。
AQS的获取锁过程分析
线程通过CAS将state从0设置为1,如果设置成功,说明获取锁成功,并且将exclusiveOwnerThread指向自己;
当调用lock方法时,公平锁和非公平锁的实现有区别
非公平锁的实现:线程会直接进行CAS操作去设置state,如果成功就获取到锁,如果失败,继续调用NonFairSync的acquire(1)方法
公平锁的实现:线程会直接调用FairSync的accquire(1)方法,这两个方法都调用了AQS的方法
public final void acquire(int arg) {
//第一步尝试获取锁
//如果成功了,就可以执行自身的逻辑了
//如果失败了,执行下一步addWaiter()方法,也就是将自身线程包装成Node加入到队列中
//第三步 死循环获取锁,如果失败则将线程挂起
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里需要看一下tryAcquire方法,首先是非公平锁
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取AQS的状态
int c = getState();
//如果等于0,说明没有线程获取到锁,通过CAS尝试获取锁,跟一开始的逻辑一模一样
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果不等于0,说明有线程已经获取到锁了,这里判断AQS的独占锁指针是不是指向自己
//如果是,说明是重入,这里就是重入的实现
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;
}
下面看一下公平锁的实现
protected final boolean tryAcquire(int acquires) {
//前面的逻辑都一样
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//差别就在这里,多了一个hasQueuedPredecessors方法,这是跟非公平锁唯一的区别
//也就是为什么叫公平锁体现在这里。
//这里判断有没有别的线程比他更早来,如果返回true说明有,肯定就直接获取失败了
//如果没有别他更早来的线程,那么自己就可以去尝试获取锁了
if (!hasQueuedPredecessors() &&
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;
}
下面看一下hasQueuedPredecessors方法
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//这里就是干了这样一件事,判断头节点的下一个节点是不是自己
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
总结一下,AQS中维护了一堆wait线程组成的等待队列,凡是进入了这个队列的线程,之后就会按顺序一个一个获取到锁,执行逻辑,也就是将他们串行化了,那么公平和非公平体现在什么地方呢?
就是tryAcquire这里,公平锁是说新线程进来对于队列中的线程是公平的,如果队列中有等待线程,它就直接往后排,而非公平锁是,新线程对于队列中的等待线程是不公平的,可能存在队列中的头节点释放掉锁之后唤醒下一个线程,结果有一个新的线程进来同时获取锁,这个时候他们机会是平等的,因此说这是非公平锁。
下面看一下addWaiter方法
private Node addWaiter(Node mode) {
//将当前线程封装成node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//如果tail不为空,也就是队列中已经有一些等待线程了,直接加入尾部后返回即可。
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果tail为空,就会走到这里来
enq(node);
return node;
}
private Node enq(final Node node) {
//死循环,为什么死循环呢?往下看
for (;;) {
//拿到tail,第一次循环这里肯定是null
Node t = tail;
if (t == null) { // Must initialize
//第一次进来,将一个空的节点作为头和尾节点,看一下Node的构造函数和属性
//可以发现头节点的waitstatus=0
if (compareAndSetHead(new Node()))
tail = head;
} else {
//第二次进来就到了这里
node.prev = t;
//将node节点插入到队列的最后
//这里为什么这么做呢?向下看
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
解答上面的问题,能这么设计就有他的原因,看一下死循环获取锁的方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//这里是死循环,是核心
for (;;) {
//获取node节点的prev节点,如果没有直接报错
final Node p = node.predecessor();
//判断p是不是头节点,如果是,尝试获取锁
//如果能够成功,说明head要么取消,要么已经释放了锁
//所以将当前节点设置为头节点,也就是说删除头节点的操作是在这里执行的
//并不是在释放锁的时候执行的
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 将node的状态改为singal之后挂起,等待有线程唤醒他之后
// 就可以继续循环获取锁了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//这里就是线程可能会被中断,如果中断了就从队列中去除掉
if (failed)
cancelAcquire(node);
}
}
//node节点获取锁失败就会进入这个方法,第一次进入的时候,pred是头节点,并且ws=0
//这个方法的源码先看else块,再看第一个if,再看第二个if
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
//第二次走这里
//直接返回true,那么就会调用park方法,线程阻塞
return true;
if (ws > 0) {
//这里就是当node的前置节点不是头节点并且状态是已取消状态
//那么就是去找下一个正常的前置节点,这里就是维护一下链表的关系
//之后return false 同样是要进行二次循环
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//第一次进来走这里,把头节点的ws设置为-1,然后return false
//也就是外面的循环逻辑会走第二遍,那么第二遍进来的时候,头节点的ws已经是-1了
//就会走第一个if
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
释放锁的原理
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//尝试释放锁,成功之后,准备唤醒头节点的下一个正常节点
if (tryRelease(arg)) {
Node h = head;
//如何代码没有看仔细,会对头节点的waitStatus状态有疑惑
//以为头节点是一个空姐点,初始状态为0
//但是经过了shouldParkAfterFailedAcquire方法之后,状态就是变成-1
//所以这里是true,正常进行释放操作
if (h != null && h.waitStatus != 0)
//这个方法就是真正的唤醒头节点的下一个正常的节点
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//重入性质
int c = getState() - releases;
//如果当前线程不是AQS的独占锁线程直接报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//直到全部释放完成,将独占锁线程设置为null,释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//重入锁就一直 -- ,然后CAS设置状态就好了
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
//获取头节点的状态
int ws = node.waitStatus;
if (ws < 0)
//这里将头节点的状态变成0
//等待后置节点将其替换
compareAndSetWaitStatus(node, ws, 0);
// 获取头节点的下一个节点
Node s = node.next;
// s如果是null,或者状态是已取消的状态,就从尾部获取下一个正常的可以唤醒的节点
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;
}
// 通过上面的计算获取到的节点,在这里进行唤醒
// 存在当前node既是head也是tail的情况,这里的thread就是null
if (s != null)
LockSupport.unpark(s.thread);
}
整个ReentrantLock的源码分析就是这样,笔者在看了这些源码大概几十次之后(但是都是粗略看的,惭愧,认真看也就是两次,一次是开始写这篇文章,另外一次就是修改第一次写的时候一些错误的地方),终于看懂了,其实不是很难,相反理解之后反而觉得非常之简单,自身的收获也很大。欢迎大家对我的分析进行指正,大家一起进步!!!
原创不易,且行且珍惜。