从类注释可以得到的信息有:
- 可重入互斥锁,和 synchronized 锁具有同样的功能语义,但更有扩展性;
- 构造器接受 fairness 的参数,fairness 是 true 时,保证获得锁时的顺序,false 不保证;
- 公平锁的吞吐量较低,获得锁的公平性不能代表线程调度的公平性;
- tryLock() 无参方法没有遵循公平性,是非公平的(lock 和 unlock 都有公平和非公平,而 tryLock 只有公平锁,所以单独拿出来说一说)。
补充一下第二点,ReentrantLock 的公平和非公平,是针对获得锁来说的,如果是公平的,可以保证同步队列中的线程从头到尾的顺序依次获得锁,非公平的就无法保证,在释放锁的过程中,我们是没有公平和非公平的说法的
1.结构
Reentrantlock 继承关系,核心成员变量及主要构造函数:
public class ReentrantLock implements Lock, java.io.Serializable {
// Sync 同步器提供了所有的加锁,释放锁的机制
private final Sync sync;
// Sync 继承了 AQS,使用了 AQS 的 state 字段代表当前锁的计算
abstract static class Sync extends AbstractQueuedSynchronizer{...}
// 非公平锁,继承了Sync
static final class NonfairSync extends Sync{...}
// 公平锁,继承了Sync
static final class FairSync extends Sync{...}
//------------------------------构造函数-------------------------------------
// 无参数构造器,即默认是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据传入的参数决定是公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
Lock接口
Lock接口,定义了锁的相关方法
public interface Lock {
// 上锁
void lock();
// 可中断锁
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁
boolean tryLock();
// 设定尝试时间,过期返回
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 创建条件队列
Condition newCondition();
}
下面,我们就来看看 Reentrantlock 中定义的那几个锁相关的内部类
1.1 Sync
Sync 直接继承 AQS,提供了无论公平/非公平锁都必须的方法。比如锁状态相关方法,锁释放,获取条件队列等。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 给子类(FairSync,NonfairSync)实现
abstract void lock();
//.....
}
state 相关方法
锁的基础 state 字段,直接调用AQS的方法
// 锁是否被持有
final boolean isLocked() {
return getState() != 0;
}
// 是否是当前线程持有锁
protected final boolean isHeldExclusively() {
// 该方法在AbstractOwnableSynchronizer中定义
return getExclusiveOwnerThread() == Thread.currentThread();
}
// 持有锁的线程是谁
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 线程持有同一个锁的数量
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
nonfairTryAcquire()
尝试获得非公平锁,分为以下三种情况:
- 锁未被占有,尝试自己拿锁
- 锁已被自己获取,重入,state+acquire(有上限)
- 拿锁失败,进入同步队列
这里需要注意一点,公平锁与非公平锁的 tryAcquire() 方法是不同的,非公平锁的 tryAcquire() 在 NonfairSync() 中,但的它的 tryAcquire() 核心逻辑(nonfairTryAcquire)在 Sync 中实现,而公平锁的 tryAcquire 完全是在 FairSync 中实现。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取锁状态
int c = getState();
// 同步器的状态是 0 表示同步器的锁没有人持有
if (c == 0) {
// 当前线程,尝试获取锁(CAS修改state)
if (compareAndSetState(0, acquires)) {
// 修改成功,拿到锁,标记当前持有锁的线程是谁
setExclusiveOwnerThread(current);
return true;
}
}
// 锁已经被占有,且刚好是当前线程,这里表重入
else if (current == getExclusiveOwnerThread()) {
// 体现可重入性。当前线程持有锁的数量 + acquires
int nextc = c + acquires;
// int 是有最大值的,<0 表示持有锁的数量超过了 int 的最大值
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 重新设置state,该放在AQS中定义
setState(nextc);
return true;
}
// 拿锁失败,线程进入同步队列
return false;
}
从这个方法中我们还可以看到 Reentrantlock 是可重入的。可重入性说的是线程可以对共享资源重复加锁,对应的,释放时也可以重复释放。
- 对于 ReentrantLock 来说,在获得锁的时候,state 会加 1,重复获得锁时,不断的对 state 进行递增即可,比如目前 state 是 4,表示线程已经对共享资源加锁了 4 次
- 线程每次释放共享资源的锁时,state 就会递减 1,直到递减到 0 时,才算真正释放掉共享资源。
tryRelease()
尝试释放锁,非公平和公平锁都的释放都是该方法。也是分为三种情况:
- 当前线程不持有锁,报错
- state=0,将 owner 置为 null,表示释放成功
- state!=0,表示是重入锁且没释放完,return false
protected final boolean tryRelease(int releases) {
// 当前同步器的状态减去释放的个数,参数releases一般为 1
int c = getState() - releases;
// 当前线程根本都不持有锁,报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果 c 为 0,表示当前线程持有的锁都释放了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 如果 c 不为 0,那么就是可重入锁,并且锁没有释放完,用 state 减去 releases 即可,无需做其他操作
setState(c);
return free;
}
newCondition()
创建一个条件队列
final ConditionObject newCondition() {
return new ConditionObject();
}
下面就看看非公平锁 NonFairSync 与公平锁 FairSync 的差异到底在哪里,何为公平何为非公平。
1.2 非公平锁:NonfairSync
非公平锁的非公平体现在两处:
- 线程未进入同步队列就有机会获取到锁:在 lock 方法和 tryAcquire 方法一共有2次尝试修改state去获得锁
- 线程已进入同步队列非队二有机会获取到锁:tryAcquire 不会校验当前线程在同步队列的位置(这里可以对比下面FairSync的 tryAcquire 方法),谁都有机会修改state去获得锁。
PS:说的更直接点就是,新线程或阻塞时间短可能比阻塞很久的线程先运行。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 获取非公平锁
final void lock() {
// cas给state赋值
if (compareAndSetState(0, 1))
// cas赋值成功,代表拿到当前锁,记录拿到锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
// acquire是AQS的方法,会再一次尝试获得锁,若还失败就会进入到同步队列中
acquire(1);
}
// 尝试获取非公平锁。该方法会在lock拿锁失败后,在acquire方法中调用
protected final boolean tryAcquire(int acquires) {
// 直接调用父类Sync的nonfairTryAcquire方法
return nonfairTryAcquire(acquires);
}
}
1.3 公平锁:FairSync
公平锁的公平体现在:
- 新线程必须进入同步队列,接收同步器调度
- 在同步队列中,只有队二线程才有机会运行
PS:说的更直接点就是,无论新老线程,都必须遵守FIFO谁先阻塞谁先运行。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 获取公平锁
// 相较于非公平锁的lock方法,没了一进来就尝试修改state
final void lock() {
// acquire 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待
acquire(1);
}
// 尝试获取非公平锁
// 相较于非公平锁的nonfairTryAcquire,这里是必须进入了同步队列才有机会修改state拿锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors 是实现公平的关键!!
// 它会判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点)
// 如果是(返回false),符合先进先出的原则,可以获得锁
// 如果不是(返回true),则继续等待
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;
}
}
这里小结一下,公平性和非公平指的线程得到锁的机制(lock & tyrAcquire),释放锁时没有差异(tryRelease)。如果同步队列中的线程按照阻塞的顺序得到锁,我们称之为公平的,反之是非公平的。
公平的底层实现是 ReentrantLock 的 tryAcquire 方法(调用的是 AQS 的 hasQueuedPredecessors 方法)里面实现的,要释放同步队列的节点时(或者获得锁时),判断当前线程节点是不是同步队列的头节点的后一个节点,如果是就释放,不是则不能释放,通过这种机制,保证同步队列中的线程得到锁时,是按照从头到尾的顺序的。
2.方法 & api
Reentrantlock 中的方法其实没什么说的,因为都是基于AQS框架,具体在这里体现为直接调用Sync
2.1 加锁:lock
public void lock() {
sync.lock();
}
2.2 尝试拿锁:tryLock
// 无参构造器
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
// timeout 为超时的时间,在时间内,仍没有得到锁,会返回 false
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
2.3 释放锁:unlock
public void unlock() {
sync.release(1);
}
2.4 条件队列:newCondition
public Condition newCondition() {
return sync.newCondition();
}
3.Reentrantlock与synchronized区别
- synchronized 是 JVM 层面实现的(C++写的);ReentrantLock 是 JDK 代码层面实现(Java写的)
- sync在优化之前是直结调用os函数实现阻塞,但优化后:volatile+自旋+CAS在用户态实现了线程安全
- reentrantlock通过AQS在代码级别(volatile+自旋+CAS)实现了线程控制,保证了线程安全
- synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
- synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果(tryLock)
- synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间(tryLock(timeUnit.S)
- synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁(new Reentrantlock(true))
- synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法(newCondition)