基本使用
ReentrantLock,位于java.util.concurrent
包,于JDK1.5引入,一种可重入互斥Lock ,其基本行为和语义与使用synchronized方法和语句访问的隐式监视器锁相同,但具有扩展功能。
ReentrantLock的使用也很简单,在源码注释中可以看到使用的推荐方式:
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
具体使用代码如下:
package com.starsray.test.lock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLock {
private final static ReentrantLock lock = new ReentrantLock();
static int threadCount = 5;
static int counter = 0;
static int threshold = 1000;
private static final CountDownLatch latch = new CountDownLatch(threadCount);
public static void m() {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < threshold; j++) {
m();
}
latch.countDown();
}, "Thread-" + i).start();
}
latch.await();
System.out.println("count:" + counter);
}
}
源码分析
ReentrantLock由上次成功锁定但尚未解锁的线程拥有。当锁不被另一个线程拥有时,调用lock的线程将返回,成功获取锁。如果当前线程已经拥有锁,该方法将立即返回。可以使用方法isHeldByCurrentThread
和getHoldCount
来检查。
ReentrantLock除了支持synchronized
的语义之外,还支持公平锁和非公平锁的拓展,在非公平锁模式下与synchronized
相同,也是其默认的锁模式。构造函数接受一个可选的公平参数,当设置为true时表示为公平锁。
ReentrantLock基于AQS实现,内部维护了一个CLH队列,所谓公平非公平,指的是线程是否按照入队的顺序进行锁的获取。需要注意的是,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。另请注意,不定时的tryLock()方法不遵守公平设置。如果锁可用,即使其他线程正在等待,它也会成功。
查看ReentrantLock的类图结构,其实现了Lock
接口,其中定义了关于锁的一些基本操作,包含了Sync
、FairSync
、NonfairSync
这几个内部类。从名称也可以看出FairSync
、NonfairSync
分别实现了公平锁和非公平锁,二者同时继承自,作为锁实现的Sync
,并且Sync
继承自AbstractQueuedSynchronizer
。
接下来对ReentrantLock的源码进行分析。
Sync(基础类)
ReentrantLock实现的基础依赖于Sync类,一个静态内部抽象类,继承自AbstractQueuedSynchronizer
,使用AQS中的state状态值来记录持有锁的次数,为下面的公平锁和非公平锁扩展定义好了基础框架。
ReentrantLock中关于加锁根据场景有不同的实现,对应的lock方法分别在Lock
接口和内部类Sync
、FairSync
、NonfairSync
中进行定义和重载,而解锁的方式统一调用的是AQS中定义的变更状态方法,得益于AQS优秀的设计。如下代码所示:
public void unlock() {
// 此处release调用为synch子类对象指向Sync继承自AQS的父类方法
sync.release(1);
}
public void lock() {
// 此处lock方法为sync子类对象各自引用的调用
sync.lock();
}
AQS中实现了同步队列阻塞以及释放锁的能力,开放了子类关于锁的添加和释放的方法,tryAcquireShared
、tryAcquire
、tryRelease
、tryReleaseShared
,因此ReentrantLock中也是如此,由于其为互斥锁实现,因此只用重写tryAcquire
、tryRelease
方法的实现即可。这在Sync类中分别对应:
// 默认实现了非公平锁的加锁条件
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果同步状态位为0,当前线程尝试通过CAS去修改状态位
if (compareAndSetState(0, acquires)) {
// 如果CAS成功,设置当前线程为独占线程,返回true
setExclusiveOwnerThread(current);
return true;
}
}
// 如果不为0,判断当前线程是否为独占线程
else if (current == getExclusiveOwnerThread()) {
// 如果再次获取锁,记录重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow 溢出
throw new Error("Maximum lock count exceeded");
// 更新状态位
setState(nextc);
return true;
}
// 如果线程首次即没有获得锁,也不满足重入条件,返回false,获取锁失败
return false;
}
// 使用模板方法设计模式,该方法在AQS的release方法中被调用,作为判断是否释放锁状态的前置条件
protected final boolean tryRelease(int releases) {
// 每次释放锁对状态为进行递减
int c = getState() - releases;
// 如果当前线程不是队列首线程,抛出IllegalMonitorStateException异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 当同步状态位state==0时,返回释放锁成功
boolean free = false;
if (c == 0) {
free = true;
// 当前独占线程为null,即没有线程占用
setExclusiveOwnerThread(null);
}
// 更新同步状态位为初始值
setState(c);
return free;
}
Sync中tryRelease
对应为父类AQS中的实现,nonfairTryAcquire
对应子类中的公共实现,父类AQS中tryAcquire
的实现分别在FairSync
和NonfairSync
子类中进行实现。
FairSync(公平锁)
由于ReentrantLock基于FIFO的队列来实现线程排队等待,公平锁中的tryAcquire方法,当CPU进行调度时,每次按照队列的排队顺序进行同步状态为值的判断,尝试锁的获取,因此实现了先到先得的公平性。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 调用AQS的acquire方法,传入参数state值为1
// 是否将当前线程加入到等待队列中,取决于tryAcquire的返回结果
// 如果尝试获取锁或者添加到队列失败,将中断当前线程操作
acquire(1);
}
// 尝试获取锁的公平锁实现,如果不是队列首线程将获取锁失败
protected final boolean tryAcquire(int acquires) {
// 获取当前运行线程
final Thread current = Thread.currentThread();
// 获取AQS的同步状态值
int c = getState();
if (c == 0) {
// hasQueuedPredecessors方法的执行结果:如果当前线程之前有排队线程,则为true ,如果当前线程位于队列的头部或队列为空,则为false
// 如果同步状态为0,并且当前队列没有线程排队,并且CAS修改状态位成功则尝试获取锁成功
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 设置当前线程为独占拥有线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果 c != 0,且当前线程为独占线程,记录当前线程重入锁的次数。
int nextc = c + acquires;
if (nextc < 0)
// 冲入次数溢出了,超过了int表示的最大值
throw new Error("Maximum lock count exceeded");
// 更新AQS同步状态位的值
setState(nextc);
return true;
}
return false;
}
}
NonfairSync(非公平锁)
关于非公平锁的实现也是相当巧妙,基于队列实现公平锁很简单,如何实现非公平锁呢?如下代码所示:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 在线程入队前先进性一次CAS尝试,如果成功,则将当前线程设置为独占模式的执行线程
// 其余线程继续保持队列排队等待状态,作者也在注释中说明了锁的公平性并不能保证线程调度的公平性
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果CAS失败,则进入队列
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 调用基础类中的实现
return nonfairTryAcquire(acquires);
}
}
总结
- ReentrantLock实现了synchronized相同的语义,并且对其进行了扩展,synchronized借助于操作系统层面互斥信号量来实现互斥锁,ReentrantLock借助于AQS抽象同步队列框架来实现锁。
- 公平锁与非公平锁,二者的区别在于,公平锁直接依赖队列的顺序进行线程的执行与挂起,而非公平锁在CPU进行调度时直接进行一次CAS操作,如果成功则获得调度优先级,失败则进入队列等待。
- 在锁的获取方面使用synchronized的锁往往会被一个线程多次获取,而ReentrantLock提供的锁在线程执释放锁后,如果再次获得锁就需要进入队列等待。
参考资料:
- jdk1.8.0_311 ReentrantLock源码
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/275732.html