简介
ReentrantLock是Java的JUC工具包下一个重要的类,相对于synchronized,它有如下特点:
- 可中断(在获取锁失败后,阻塞等待锁的过程,这个阻塞状态,是可以被打断的,打断后会抛出异常)
- 可超时(有时限的等待)
- 可公平(可以设置为公平锁,让来获取线程的锁先到先得)
- 支持多个条件变量(synchronized对对象加锁,关联操作系统的一个Monitor,一个Monitor只有1个WaitSet,所有wait的线程都到这个WaitSet中等待,而ReentrantLock相当于可以有多个WaitSet)
ReentrantLock和synchronized都支持可重入。
基本用法是创建ReentrantLock对象,该对象调用lock()获得锁,unlock()释放锁。
public static void main(String[] args){
//获取 ReentrantLock对象
ReentrantLock reentrantLock = new ReentrantLock();
//获取锁
reentrantLock.lock();
try{
//...
} finally{
//释放锁
reentantLock.unlock();
}
}
调用lock()获得锁,是不可打断的,也无超时,如果竞争锁失败,会一直阻塞等待。lockInterruptibly()是可打断的。tryLock(long timeout, TimeUnit unit)可设置超时等待。tryLock()无参数是立即返回结果,获得到锁就返回true,获得不到也不阻塞,直接返回false。
源码时间
lock()
进入ReentrantLock的lock()方法
public void lock() {
sync.lock();
}
再进sync.lock(),这里看非公平实现
NonfairSync有这四个关键属性:
- exclusiveOwnerThread是持有锁的线程,state、head、tail都来自其继承的父类AQS(AbstractQueuedSynchronizer),state是一个状态值,head和tail都是节点指针。
lock()方法首先通过cas将state从0改为1,0表示无锁,1表示有锁,如果成功,设置当前线程为exclusiveOwnerThread。如果失败,进入acquire(1)。
cas是为了保证原子操作,避免出现线程并发安全问题。比如有一个共享变量count=0,现在用cas将其修改为1,在修改时,会去看看count值是不是还是原来的0,如果是,证明没有被其它线程修改过,那么可以成功改为1;如果不是,证明被其它线程修改过了,那么这次cas操作失败,放弃修改。
acquire(1)
首先走tryAcquire(1),尝试获得锁,如果成功,方法结束。
tryAcquire(1)
看tryAcquire内部逻辑,同样是看非公平实现。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); //获取state值
if (c == 0) { //如果state为0,无锁状态
if (compareAndSetState(0, acquires)) { //尝试cas将state从0改为1
setExclusiveOwnerThread(current); //cas成功,设置当前线程为owner
return true;
}
}
//如果已经有线程加锁了,并且当前线程是owner,证明这是重入
else if (current == getExclusiveOwnerThread()) {
//将state值+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 如果还没有线程加锁,或者加锁的就是自己这个线程,那么tryAcquire(1)是成功的,否则失败。
tryAcquire()失败,会接着走acquireQueued(addWaiter(Node.EXCLUSIVE), 1),相当于让线程进入阻塞队列,具体是用NonfairSync中的head和tail指针连接双向链表实现。
addWaiter(null)
Node.EXCLUSIVE是null。这是第一次进addWaiter()方法,链表还是空的。head和tail也是指向null。
private Node addWaiter(Node mode) {
//创建当前线程节点
Node node = new Node(Thread.currentThread(), mode);
//链表为空,当前pred为null,不走if
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//进入enq(node)
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) { //死循环
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
t指针指向链表尾指针tail,满足t==null,compareAndSetHead(new Node())创建一个节点,让head指向它。
cas成功,【tail = head】,tail和head指针一起指向这个新节点,第一次循环结束。第二次循环开始,【Node t = tail;】t指针和tail指针一起指向新节点,这次t != null了,进入else块,【node.prev = t;】 让node的头指针指向新节点,cas让链表尾指针tail指向node,cas成功后,【t.next = node;】让新节点的尾指针指向node,最后返回t指向的新节点。
经过这次enq(),链表结构变成:head --> node(null) <--> node(thread) <- tail
第一次进入addWaiter()方法,即链表为空时,会创建两个节点,一个是哑节点(占位节点),一个是实际节点(有对应线程的节点)。后面进入addWaiter()方法,都是创建一个实际节点。
enq()结束,回到addWaiter()方法,返回当前线程节点。
acquireQueued(node,1)
node为当前线程节点
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //死循环
final Node p = node.predecessor(); //获取node前驱节点
//前驱节点是head指向节点(占位节点),证明当前节点是链表中第一个
//有实际线程对应的节点,tryAcquire(1)再次尝试获取锁
if (p == head && tryAcquire(arg)) {
//成功获得到锁,让head指向node,即让node作为新的占位节点,把node中的
//线程信息清空,node头指针指向null
setHead(node);
//将前驱节点next指针指向null
p.next = null; // help GC
//上两行代码等同于将原来的占位节点从链表中移除,让node取而代之。
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
如果能够成功获取到锁,线程就可以成为owner了,这个node也没用了,去做占位节点。tryAcquire如果失败或者不是第一个有效节点,执行第二个if块内容,第一次进shouldParkAfterFailedAcquire(),会把前置节点状态设置为-1,表示它有责任去唤醒它的后继节点(因为它的后继节点拿不到锁要准备阻塞等待了),返回false,然后回到acquireQueued()继续循环,如果是第一个有效节点,又有一个tryAcquire尝试获取锁的机会,失败,又到shouldParkAfterFailedAcquire(),第二次进,就返回true了,就会执行parkAndCheckInterrupt(),线程在此阻塞等待。
来数数看,线程在cas获取锁失败后,一共还有几次tryAcquire()尝试获取锁的机会,acquire中1次,如果是链表中第一个竞争失败节点,acquireQueued中有2次,所以在阻塞前,它最多会尝试3次。(事不过三啊)acquireQueued过程没有被打断并且成功拿到了锁,返回false,acquire()结束,lock()结束,如果被打断了,最后拿到锁返回true,回到acquire(),把自己打断。
unlock()
解锁流程基于以下情况:
Thread-0持有锁,Thread-1、Thread-2、Thread-3来获取锁都失败阻塞,并且head指向的占位节点及Thread-1、Thread-2的status值都为-1,表示有责任唤醒它的后继节点。
现在,Thread-0调用unlock()方法释放锁。
ReentrantLock的unlock()方法,调用同步器的release(1)
release(1)
release(1)在AQS中
tryRelease(1)
进入tryRelease(1),选择以下实现
如果有锁重入,(一个线程加了多次锁)没解完,返回false。只剩一把锁,解一次解完了,返回true。
protected final boolean tryRelease(int releases) {
//获取state值并-1
int c = getState() - releases;
//校验调用解锁方法的线程是否是拥有锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//解锁成功,设置owner为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//状态值存入
//如果state减完后为0,解锁成功,返回true。
//如果不为0,证明有锁重入,还需要多次解锁,返回false。
setState(c);
return free;
}
这里假设Thread-1没有锁重入,解锁成功,tryRelease(1)返回true。回到release(1)中,因为head != null 并且head的status为-1,所以调用unparkSuccessor(head),将head指向的占位节点的status改为0,并唤醒后继节点,即unpark第一个有效节点,也就是Thread-1所在节点。
unparkSuccessor(node)
private void unparkSuccessor(Node node) {
//获取head状态
int ws = node.waitStatus;
//head状态为-1,进入这个if
if (ws < 0)
//将head指向节点的status改为0
compareAndSetWaitStatus(node, ws, 0);
//拿到Thread-1所在节点
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)
//Thread-1被唤醒
LockSupport.unpark(s.thread);
}
还记得线程获取锁失败是在哪阻塞的吗?
那当然是在获取锁的地方啦,就是acquireQueued()中的parkAndCheckInterrupt()处
acquireQueued()
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //死循环
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
现在Thread-1被唤醒了,它就接着走循环,【node.predecessor()】获取它的前驱节点,就是head,所以满足if了,进入tryAcquire(1)尝试获取锁,在没有竞争的情况下,Thread-1就成功拿到锁了,把owner设置为Thread-1,进入【if (p == head && tryAcquire(arg)) {}】这个if块,将Thread-1原来所在的节点设置为头节点(新的占位节点),并把节点内的线程信息清空,原来的占位节点就被移除出链表了。(原本是head --> 占位 <--> node,在setHead(node)中让node.prev = null,在if块中燃p.next = null,等于把占位节点的前后指向都断开了)
于是,变成了酱紫:
如果Thread-1被唤醒后有竞争,即有其他线程来跟他抢着加锁,不巧,Thread-1又没抢过,那么它又在acquireQueued中循环尝试三次,又再次park阻塞住了。(当然,这时之前的占位节点就保住它的地位啦)