目录
注意:本文参考 java并发编程的艺术
从ReentrantLock的实现看AQS的原理及应用 | JavaGuide
重入锁ReentrantLock
ReentrantLock简介
重入锁ReentrantLock, 顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
回忆在同步器一节中的示例(Mutex), 同时考虑如下场景当一个线程调用Mutex的lock() 方法获取锁之后,如果再次调用lock()方法,则该线程将会被自己所阻塞,原因是Mutex在实现 try Acquire(int acquires)方法时没有考虑占有锁的线程再次获取锁的场景,而在调用
try Acquire(int acquires)方法时返回了false, 导致该线程被阻塞。简单地说,Mutex是一个不支持 重进入的锁。而synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方 法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,而不像Mutex由于获 取了锁,而在下一次获取锁时出现阻塞自己的情况。
ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方 法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先 被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线 程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock提供了一个构造函数,能够控制锁 是否是公平的。
事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS作为 唯一的指标,公平锁能够减少“饥饿“发生的概率,等待越久的请求越是能够得到优先满足。
ReentrantLock与Synchronized区别
// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
ReentrantLock 与 AQS 的关联
ReentrantLock 是如何通过公平锁和非公平锁与 AQS 关联起来呢? 我们着重从这两者的加锁过程来理解一下它们与 AQS 之间的关系(加锁过程中与 AQS 的关联比较明显,解锁流程后续会介绍)。
// java.util.concurrent.locks.ReentrantLock#NonfairSync
// 非公平锁
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...
}
public ReentrantLock() {
sync = new NonfairSync();
}
当然你也可以用如下构造方法来指定使用公平锁:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
内部有一个静态内部类NonfairSync继承了Sync,Sync继承了AQS
这块代码的含义为:
1 若通过 CAS 设置变量 State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。
2 若通过 CAS 设置变量 State(同步状态)失败,也就是获取锁失败,则进入 Acquire 方法进行后续处理。
再看下公平锁源码中获锁的方式:
// java.util.concurrent.locks.ReentrantLock#FairSync
static final class FairSync extends Sync {
...
final void lock() {
acquire(1);
}
...
}
看到这块代码,我们可能会存在这种疑问:Lock 函数通过 Acquire 方法进行加锁,但是具体是如何加锁的呢?
结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了 Acquire 方法,而 Acquire 方法是 FairSync 和 UnfairSync 的父类 AQS 中的核心方法。
非公平锁的加锁流程
加锁:
通过 ReentrantLock 的加锁方法 Lock 进行加锁操作。
会调用到内部类 Sync 的 Lock 方法,由于 Sync#lock 是抽象方法,根据 ReentrantLock 初始化选择的公平锁和非公平锁,执行相关内部类的 Lock 方法,本质上都会执行 AQS 的 Acquire 方法。
AQS 的 Acquire 方法会执行 tryAcquire 方法,但是由于 tryAcquire 需要自定义同步器实现,因此执行了 ReentrantLock 中的 tryAcquire 方法,由于 ReentrantLock 是通过公平锁和非公平锁内部类实现的 tryAcquire 方法,因此会根据锁类型不同,执行不同的 tryAcquire。
tryAcquire 是获取锁逻辑,获取失败后,会执行框架 AQS 的后续逻辑,跟 ReentrantLock 自定义同步器无关。
解锁:
通过 ReentrantLock 的解锁方法 Unlock 进行解锁。
Unlock 会调用内部类 Sync 的 Release 方法,该方法继承于 AQS。
Release 中会调用 tryRelease 方法,tryRelease 需要自定义同步器实现,tryRelease 只在 ReentrantLock 中的 Sync 实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
释放成功后,所有处理由 AQS 框架完成,与自定义同步器无关。
通过上面的描述,大概可以总结出 ReentrantLock 加锁解锁时 API 层核心方法的映射关系。
非公平锁的具体实现与可重入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
ReentrantLock以通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现为例,获取同步状态的代码如代码清单
注意:ReentrantLock的NonfairSync类的tryAcquire方法直接调用了Sync类的nonfairTryAcquire方法
该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来 决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加(state++)并返回 true, 表示获取同步状态成功。
公平与非公平获取锁的区别
公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合 请求的绝对时间顺序,也就是FIFO。
与公平锁相比,非公平锁nonFairSync的lock实现略有不同。
公平锁的lock实现是直接调用acquire(1),而非公平锁的lock实现会先尝试CAS修改state,如果能够将state从0改成1,那么说明当前线程获取锁,既然获取锁,那么便直接插队setExclusiveOwnerThread(Thread.currentThread())。
如果CAS操作失败,再走正常流程,调用父类函数acquire(1)。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final void lock() {
// 获取锁的第一步便是调用acquire,里面的参数1代表state需要增加1。
acquire(1);
}
还有nonfairTryAcquire(int acquires)方法(nonFairSync的tryAcquire方法直接调用了sync的nonfairTryAcquire方法),对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同
FairSync的tryAcquire方法如下
该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
首先要知道,hasQueuedPredecessors返回true代表有别的线程在CHL队列中排了当前线程之前,当前线程需要入队等待;
返回false代表队列中没有节点或者当前线程处于CHL队列的第一个线程。
先判断head
是否等于tail
,如果head
和tail
不相等,说明队列中有等待线程创建的节点(第一个条件为true,由于后面是||,继续后面的判断)
接着判断head
的后置节点,这里肯定会不是null
,为false。如果此Node
节点对应的线程和当前的线程不是同一个线程,那么则会返回true
,代表head的next节点不是当前线程的,不能获取锁。
如果队列中只有一个Node
节点,那么head
会等于tail
,此时直接返回false,可以获取锁。
即加入了同步队列中当前节点是否有前驱节点的判断,如果该 方法返回true, 则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释 放锁之后才能继续获取锁。
公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁出现了一个线程连续获取锁的情况。
为什么会出现线程连续获取锁的情况呢?回顾nonfairTry Acquire(int acquires)方法,当一个线程请求锁时只要获取了同步状态即成功获取锁。在这个前提下,