一、ReentrantLock介绍
ReentrantLock
是一个和synchronized
拥有相同语义但同时扩展了额外功能的可重入互斥锁实现。ReentrantLock
将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock()
的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用isHeldByCurrentThread()
和 getHoldCount()
方法来检查此情况是否发生。
ReentrantLock
有公平锁和非公平锁两种,通过构造器传入一个boolean fair
参数指定,该参数是可选的,默认为false,也就是说,默认是非公平锁实现。但请注意,这里所说的公平与非公平,只是说获取锁的时候是否顺序进行,并不保证线程调度的公平性。因此,使用公平锁的多个线程中的一个可能会连续多次获得它。公平锁即在线程相互争用锁的情况下,它会更偏向于让等待时间最长的那个线程获得锁(队头),但相比较非公平锁,使用多个线程访问公平锁的程序吞吐量比较低,或者明显更慢。
通常情况下建议将释放锁的操作放置在finally{}
语句块中,如下面代码:
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
复制代码
二、源码分析
查看ReentrantLock
源码,发现该类只有一个Sync
的成员变量:
private final Sync sync;
复制代码
Sync
为继承自AQS
的一个同步器实现,其内部同时提供了一个lock()
抽象方法供子类实现,完成获取锁操作。Sync
有FairSync
,NonfairSync
两个子类,分别提供公平锁和非公平锁的相关操作。
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}
static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}
复制代码
下面分别从公平和非公平两种实现探讨其获取锁和释放锁的操作:
1、公平锁如何获取锁?
final void lock() {
acquire(1);
}
复制代码
可以看出其直接调用AQS
的acquire(int)
方法获取锁,接下来看下acquire()
实现:
public final void acquire(int arg) {
// 1、首先尝试获取锁,如果获取失败,那么就假如等待队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
复制代码
接着查看FairSync
中提供的tryAcquire(int)
方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 1、如果锁还没有被别人获取,及同步状态为0
if (c == 0) {
// 2、判断是否已经有其他线程在等待获取该锁,如果没有
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 3、判断持有锁的线程是否是自己
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
复制代码
从上面代码第2步可以看出,公平锁会偏向于给队列中等待时间最长的线程优先获得锁,如果此时没有其他线程在等待,则执行CAS争抢锁资源。第3步如果该锁已经被持有,则判断持有该锁的线程是否当前线程本身,如果是,那么同步状态state递增(加锁次数)。可以看出,公平锁按等待队列顺序分配锁资源,高并发下性能,效率不高。
2、非公平锁如何获取锁?
final void lock() {
// 1、直接参与竞争,如果该锁已被其它线程持有,那么就执行else
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
复制代码
接着继续查看Sync
提供的nonfairTryAcquire()
方法,源码如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1、无需额外判断是否有其它线程处于等待状态
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
复制代码
突然,小编发现,这个nonfairTryAcquire()
方法怎么和上面讲到的tryAcquire(int)
有点相似,原来tryAcquire(int)
比nonfairTryAcquire()
多了一步判断同步队列中是否有其它线程正在等待,因此,这也是公平和非公平的区别所在,如果nonfairTryAcquire()
方法也没能获取锁,那么将被挂到同步等待队列中。
那么,我们可能会问,既然会被挂到同步队列中,那当前被挂起的这个线程后续是怎么被唤醒抢夺锁资源的呢?还是按顺序出队列吗?如果还是按顺序出队列,是不是就和公平锁一样了呢?其实很简单,还是回到下面这个方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
复制代码
首先执行tryAcquire(arg)
获取锁失败之后,会执行addWaiter()
将自己封装成Node
节点入队,接着调用acquireQueued()
方法,答案就在acquireQueued()
方法中,源码如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 1、自旋,其他参与同一个锁竞争的线程也在同时不断自旋
for (;;) {
final Node p = node.predecessor();
// 2、不断尝试获取锁资源
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 3、处理中断情况
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 由于中断或者超时,必须将状态改成cancel
if (failed)
cancelAcquire(node);
}
}
复制代码
原来非公平锁参与锁竞争失败、被挂到等待队列之后,会cas+自旋直到获取锁成功,确实很不公平,哈哈。
3、锁的释放
public void unlock() {
sync.release(1);
}
复制代码
ReentrantLock
中释放锁统一为unlock()
方法,从上面源码可以看出,每调用一次unlock()
方法,同步状态就会减一,也就是说,lock()
多少次,就要对应unlock()
多少次。
接着深入release()
方法,源码如下:
public final boolean release(int arg) {
// 1、尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 2、唤醒那些由于中断或其它情况导致waitStatus不为0的节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
复制代码
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果线程不是当前线程持有,那么就报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 该锁已经没有线程持有了,同步状态为0了,那么其它线程就可以从cas+自旋中退出了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
复制代码
三、ReentrantLock与synchronized的区别
都说synchronized
使用的是重量级锁,性能非常低下,但是新版本的jdk已经做了如偏向锁之类的优化,性能其实还可以。synchronized
属于jvm层面的加锁机制,而Lock
属于API层面上的加锁,那么它们到底有什么区别呢?
-
1、如等待可中断
持有锁的线程如果长期不释放锁,正在等待的线程可以选择放弃等待。
1.设置超时方法
tryLock(long timeout, TimeUnit unit)
,时间过了就放弃等待;2.调用
lockInterruptibly()
方法,如果线程中断了,则结束获取锁操作; -
2、
synchronized
为非公平锁,ReentrantLock
同时支持公平锁和非公平锁 -
3、
ReentrantLock
可结合Condition
条件进行使用,可分别对多种条件加锁,对线程的等待、唤醒操作更加详细和灵活,在多个条件变量和高度竞争锁的地方,ReentrantLock
更加适合
有一点需要注意就是,释放锁的操作一定要在finally
块执行,否则可能出现死锁等意外情况。
四、应用场景
参考文章: