最近在看java的LinkedBlockingQueue数据结构时,发现里面使用了ReentrantLock,为了更好的理解LinkedBlockingQueue的线程安全原理,就不得不搞清楚ReentrantLock的背后原理,本篇文章详细介绍ReentrantLock的加锁、解锁、公平锁、非公平锁的幕后故事。
ReentrantLock中有一个Sync对象sync,Sync继承自AbstractQueuedSynchronizer(AQS)。Sync的具体实现有两个NonfairSync(非公平锁)和FairSync(公平锁),继承关系如下:
因为都继承自AQS,所以无论是NofairSync还是FairSync两种锁的数据结构是一样的,主要的类成员如下:
-- head,tail:保存等待获取锁的一个链表队列,head指向链表头,tail指向链表尾
-- state:状态,如果大于0,说明锁已被使用。
-- exclusiveOwnerThread:保存占据锁的线程
ReentrantLock有两个构造函数,默认构造函数使用的是非公平锁;如果设置参数为true,则为公平锁,代码如下:
public ReentrantLock() {
sync = new NonfairSync(); //作者注:默认非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); //作者注:通过fair决定使用公平锁还是非公平锁
}
在调用ReentrantLock的lock方法时,实际上最终调用到Sync的lock方法,当锁是公平锁时,调用到FairSync的lock方法,整个获取锁的代码调用过程如下:
//作者注:FairSync的lock方法
final void lock() {
acquire(1);
}
//作者注:AQS的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//作者注: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;
}
1、调用tryAcquire方法尝试去获取锁:
-- 如果当前锁没有线程使用(state=0)并且队列里面没有等待的线程(!hasQueuedPredecessors()),则获取锁,将state置为1且exclusiveOwnerThread置为当前线程,获取成功返回true;
-- 如果锁已被当前线程获取,则将state=state+1,获取成功返回true。(这种叫做重入锁,也就是同一个线程内可以多次获取锁,当然一个线程获取了几次锁,最终也要释放几次),
-- 获取锁失败,返回false。
2、如果获取锁失败,则调用addWaiter(Node.EXCLUSIVE), arg)方法,将当前线程放到等待队列里面,等待别的线程释放锁。假如thread-0获取了锁,未释放状态下,后续到来要获取锁的线程都放到链表里面去。具体数据结构如下:
(这个图是一个大概的情况,里面涉及的一些细节没有展示,比如head指向的其实是一个空的节点,空的节点后面才跟实际的线程节点)
非公平锁和公平锁的加锁过程的唯一区别如下:
//作者注:NonFairSync的lock方法
final void lock() {
if (compareAndSetState(0, 1)) //作者注:直接尝试获取锁,有可能插队成功。
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //作者注:如果获取失败,也要乖乖的去队列排队去。
}
由2中知,公平锁在获取锁之前,先去判断有没有其他线程占用锁(state是否等于0)以及队列里面是否有等待的线程,如果有,则将当前线程加入到等待队列。
但是非公平锁不判断等待队列里是否有等待的线程,而是直接尝试去重新设置锁的状态(compareAndSetState),如果设置成功,说明锁已被释放,该线程直接占有锁。所以我的理解就是:
公平锁:所有线程都要先判断队列里是否有等待的线程,如果有,要乖乖的到队列里去排队。
非公平锁:不判断队列里是否有等待的线程,直接尝试获取锁(也就是插队),可能比队列里的线程优先获得锁。
当然,如果当前线程没有获取到锁,最终还是要乖乖的去队列里排队去(acquire(1)后续的执行过程和公平锁是一模一样的)。
锁的释放入口函数如下:
//作者注:AbstractQueuedSynchronizer里的方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//作者注:Sync里的方法
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;
}
1、tryRealease(arg)中将state置为0,且将exclusiveOwnerThread置为null,
2、具体队列里的线程重新获取锁是在什么地方调用的,我没找到。(不是unparkSuccessor这个方法)
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。
在实际使用中,Condition配合Sync进行使用,为了了解Condition和Sync的关系,我通过以下方式建立了两个Contition,运行之后进行debug,看看这两个Condition里面都有什么。
ReentrantLock lock = new ReentrantLock();
Condition cond1 = lock.newCondition();
Condition cond2 = lock.newCondition();
debug看一下对象里面的信息:
可以看到cond1和cond2共用一个Sync。所以通过上述建立Condition的方式,知道生成的数据结构如下:
等待线程和被阻塞线程的区别是:
等待线程:有权利获取锁。
被阻塞线程:没有权利获取锁,需要被唤醒后加入到AQS等待队列才有权利获取锁。
调用condition的await方法,旨在说明要对当前线程进行堵塞,并释放当前线程持有的锁,将当前线程添加到condition的阻塞队列中,调用代码如下:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//作者注:将当前线程添加到Condition的阻塞线程队里的末尾
Node node = addConditionWaiter();
//作者注:释放当前线程持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//作者注:将当前线程挂起
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//作者注:如果当前线程被唤起,尝试去获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
为了更直观的看到执行过程,我们使用简单的代码进行debug测试,写一个如下简单的代码:
public class ReentrantLockTest {
public static final ReentrantLock lock = new ReentrantLock();
public static final Condition condition = lock.newCondition();
public static class Td1 extends Thread {
ReentrantLock lock ;
Condition condition ;
public Td1(ReentrantLock lock,Condition condition){
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
lock.lock(); // 代码1
condition.await(); //代码2
System.out.println("thread-0");
lock.unlock(); //代码3
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static class Td2 extends Thread {
ReentrantLock lock ;
Condition condition ;
public Td2(ReentrantLock lock,Condition condition){
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
try {
Thread.sleep(5000); //为了保证先运行线程1,后运行线程2,在这里暂停5秒
lock.lock(); //代码4
condition.signal(); //代码5
System.out.println("thread-1");
lock.unlock(); //代码6
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Td1 td1 = new Td1(lock,condition);
Td2 td2 = new Td2(lock,condition);
td1.start();
td2.start();
}
}
1、运行到代码1:可以看到lock的debug数据如下,锁被Thread-0占据(exclusiveOwnerThread=thread-0,state=1):
2、运行到代码2:因为此时线程被挂起,为了看到具体数据,我们debug到await代码内部的int interruptMode = 0这一行,可以看到debug数据如下:thread-0被添加到condition的队列中,而锁被释放(exclusiveOwnerThread=null,state=0)
3、运行到代码4:此时thread-0还在condition队列,而锁被thread-1占据
4、运行到代码5:唤起condition,将thread-0从condition队列清除,添加到lock的等待队列;但因为此时thread-1还没有释放锁,锁还是被thread-1持有。
5、运行到代码6:此时thread-1释放锁,因为运行速度较快,没有看到锁空闲时段,就已经被thread-0捕获到:
6、此时thread-0已经被唤起,代码运行到代码3处,thread-0释放锁整个代码运行结束。
以上就是ReentrantLock和Condition的配合使用整个流程,从debug过程中,可以清晰的看到整个内部数据结构和锁持有的变化情况。
本篇文章从整体的锁的持有变化及ReentrantLock和Condition内部数据结构进行了框架性的说明,其实内部的锁实现机制有很多细节,在这里并没有展示,因为作者的目的是让大家看清楚锁是如何被获取和释放的。如果想了解具体的细节机制,可以参考下面这个作者写的,非常详细,但是需要仔细的品味,想过一遍就看懂是不太可能的。
ReentrantLock锁内部详细实现机制