1.简介
java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。下面就分析下ReentrantLock的源码。
2.实现原理
ReentrantLock就是使用AQS而实现的一把锁(对AQS不了解的:AQS深度解析),可以通过构造函数设置为公平锁和非公平锁
。它有一个内部类用作同步器是Sync,Sync是继承了AQS的一个子类,并且公平锁(FairSync类)和非公平锁(NonFairSync类)是继承了Sync的两个子类如下图。
公平与非公平的实现如下:
看它们获取锁知道,公平锁与非公平锁的区别就是:在获取锁的时候非公平锁会先利用cas修改state的值来获取锁,获取成功了就把当前线程赋值为获取锁的线程,失败则加入到队列中。
ReentrantLock的原理是:假设有一个线程A来尝试获取锁,它会先CAS修改state的值,从0修改到1,如果修改成功,那就说明获取锁成功,设置加锁线程为当前线程。如果此时又有一个线程B来尝试获取锁,那么它也会CAS修改state的值(这里是非公平锁
),从0修改到1,因为线程A已经修改了state的值,那么线程B就会修改失败,然后他会判断一下加锁线程是否为自己本身线程,如果是自己本身线程的话它就会将state的值直接加1,这是为了实现锁的可重入。如果加锁线程不是当前线程的话,那么就会将它生成一个Node节点,加入到等待队列的队尾,直到什么时候线程A释放了锁它会唤醒等待队列队头的线程。这里还要分为公平锁和非公平锁,默认为非公平锁,公平锁和非公平锁无非就差了一步。如果是公平锁,此时又有外来线程尝试获取锁,它会首先判断一下等待队列是否有第一个节点,如果有第一个节点,就说明等待队列不为空,有等待获取锁的线程,那么它就不会去同步队列中抢占cpu资源。如果是非公平锁的话,它就不会判断等待队列是否有第一个节点,它会直接前往同步对列中去抢占cpu资源。
3.源码解析
3.1获取锁
acquire是AQS中的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法的主要逻辑是
- 通过tryAcquire(ReentrantLock实现)尝试获取独占锁,如果成功返回true,失败返回false
- 如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部
- acquireQueued,将Node作为参数,通过自旋去尝试获取锁。
3.1.1尝试获取锁
tryAcquire()方法在NonfairSync和FairSync中实现的方式是不一样的,下面分开看下。
NonfairSync:tryAcquire()
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果state==0说明,没有线程在占用锁,则用cas修改state的值,并设置独占锁线程为当前线程
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//state!=0,说明有线程在占用锁,判断独占锁线程是否是当前线程。
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;
}
FairSync:tryAcquire()
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
获取锁不管是公平还是非公平主流程都是:
- 获取state的值
- 判断state是否等于
0
,ture
则说明没有线程在占用锁,则用cas修改state的值,并设置独占锁线程为当前线程。 (state==0)=false
,说明有线程在占用锁,判断独占锁线程是否是当前线程。如果是的话,尝试进行再次获取这个锁(ReentrantLock是一个可重入的锁)如果获取锁的次数没有超过上限的话(即,c + acquires > 0),则更新state的值为最终该锁被当前线程获取的次数,然后方法结束返回true;否则,如果当前线程获取这个锁的次数超过了上限则或抛出Error异常。
公平锁与非公平锁获取锁的唯一区别就是在让线程获取锁的时候多了一个!hasQueuedPredecessors()
判断方法,下面说下这个方法:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
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());
}
『hasQueuedPredecessors』方法判断是否有比当前线程等待更久准备获取锁的线程
:
a)如果有则方法结束,返回false;
b)如果没有,则说明当前线程前面没有另一个比它等待更久的时间在等待获取这个锁的线程,则尝试通过CAS的方式让当前的线程获取锁。
3.2加入队列
如果tryAcquire()返回false,则会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
首先看下addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//不为空就追加节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//执行到这里,说明队尾为null,则:队列还未初始化,队列为空
enq(node);
return node;
}
private Node enq(final Node node) {
//通过自旋添加节点都队尾
for (;;) {
Node t = tail;
//初始化head,并且让tail=head,这时队列为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;
}
}
}
}
这个方法没什么要说的就是添加节点。
主要是acquireQueued()方法,这个方法会自旋挂起等待的线程。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//如果新建的节点的前驱为head && 获取锁成功
if (p == head && tryAcquire(arg)) {
//设置head=node
setHead(node);
p.next = null; // help GC root
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) 检查下是否需要将当前节点挂起
interrupted = true;
}
} finally {
if (failed) //如果失败或出现异常,失败 取消该节点,以便唤醒后续节点
cancelAcquire(node);
}
}
//挂起线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这个方法主要逻辑:
- 获取当前节点的前驱。
- 如果新建的节点的前驱为head && 获取锁成功,就会调用setHead()方法把当前节点设置head节点。
- 检查需不需要将当前节点挂起,需要就会执行parkAndCheckInterrupt()的LockSupport.park(this)挂起线程。
- 最后,如果失败或出现异常,取消该节点,以便唤醒后续节点。
获取锁到这里就算结束了,下面来看下释放锁的源码。
3.3释放锁
我们在使用ReentrantLock是使用lock()方法获取锁,使用unlock()方法释放锁,上面已经吧公平与非公平获取锁的源码分析完毕,下面一起看看unlock()方法的实现方式。
public void unlock() {
sync.release(1);
}
//释放锁的主要逻辑
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
从上面源码知道主要释放锁的逻辑在release()方法。该方法的主要逻辑是:
- 使用tryRelease(arg)尝试释放锁,释放成功就调用unparkSuccessor(h)恢复线程。
- 如果尝试释放锁失败,返回false,我们可以自己通过自旋的方式去释放。
下面看看tryRelease(int releases)方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
这个方法也很简单,首先会判断当前释放锁的线程是不是获取到独占锁的线程
,如果不是抛出异常。
如果是,计算c = getState() - releases,c=0说明没有重入了
,可以返回true,并设置获取到锁的线程为null,表示当前线程释放锁成功。c!=0,说明锁线程处于重入状态
,需要一层一层去释放。
最后就是unparkSuccessor(Node node):
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 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;
}
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor主要就两句代码:获取后继节点,并恢复后记线程,让后继线程获取锁。
Node s = node.next;
LockSupport.unpark(s.thread);
4.总结
最后总结一下,从源码中能看到,AQS中具体实现了获取锁acquire()和释放锁release(),定义了tryAcquire()和tryRelease()方法让子类去实现具体的获取和释放的逻辑,其实主要的就是利用cas对state值的操作。
以上只是我粗略的理解,如果有理解不对、表达不到的地方,期待大家留意。
如果觉得能让你理解一二,请看我写的另一篇文章,可以对AQS有更好的理解:
CountDownLatch源码解析