1 基本介绍
ReentrantLock是可重入独占锁,同时只能由一个线程锁持有,如果其他线程想要获取锁,就会被阻塞并放入该锁的AQS同步队列中。
1.1 为什么要使用ReentrantLock
在多线程编程中,如果要多资源进行加锁,最常用的方法是使用synchronized关键字,然而synchronized关键字缺有诸多不足之处,而ReentrantLock相对而言可以解决这些不足,如:
- synchronized无法知道线程有没有成功获取到锁,而ReentrantLock可以。
- synchronized的 wait方法只能有一个条件变量,而ReentrantLock可以配合Condition的await方法实现多个条件变量,更为灵活。
1.2 ReentrantLock的常用方法
下面就是介绍一下ReentrantLock的常用方法:
方法名 | 作用 |
---|---|
lock() | 获取锁,成功后变返回,如果失败了,就会被阻塞 |
lockInterruptibly() | 与lock()方法作用一样,不同的是该方法可以对响应,如果其他线程调用当前线程的interrupt() 方法,则当前线程会抛出InterruptedException异常并返回 |
tryLock() | 在不阻塞当前线程的情况下,尝试获取锁,成功则返回true,失败则返回false |
tryLock(long timeout, TimeUnit unit) | 如果在指定的时间内未能成功获取锁,则返回false,成功则返回true |
unlock() | 状态值减去1,如果减1后状态值为0则释放锁 |
newCondition() | 获取一个条件变量 |
2 源码梳理
这里还是一样的用的 Jdk11,和网上一些以 Jdk8的资料有一些差异,但是差异不大。
2.1 构造方法
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
由构造方法可知,ReentrantLock默认为非公平锁,因为这样获取锁时省略了一些检查操作,速度快一些。如果想要使用公平锁,可以在创建ReentrantLock时传入true即可。
2.2 加锁
方法入口如下:
public void lock() {
sync.acquire(1);
}
sync是Sync对象,Sync继承了AbstractQueuedSynchronizer,所以sync.acquire(1)实际的调用了AbstractQueuedSynchronizer的acquire方法,下面进入acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法中做了2个判断,第一个判断是尝试获取锁,如果失败了则进入第二个判断,将当前线程封装为类型为Node.EXCLUSIVE的Node节点,插入AQS同步队列的尾部,并不停的循环获取锁。
另外,AQS并不实现tryAcquire方法,而是由子类实现,所以我们进入FairSync的tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
// 具体实现方法是 nonfairTryAcquire。
return nonfairTryAcquire(acquires);
}
// 以非公平的方式获取锁
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取 AQS状态值
int c = getState();
// 如果 AQS状态值为 0,则说明当前锁未被占用,则可以获取锁。
if (c == 0) {
// 这里主要是执行的 AQS的操作,以 CAS的方式将 AQS状态值设置为 1。
// 非公平锁和公平锁的差异主要是体现在这里。
if (compareAndSetState(0, acquires)) {
// 如果成功将 AQS状态值,则将 AQS中锁的持有者设置为当前线程。然后返回。
setExclusiveOwnerThread(current);
return true;
}
}
// 如果锁已经被占用了,那么就先判断当前线程是否是该锁的持有者。
else if (current == getExclusiveOwnerThread()) {
// 如果是,则将 AQS的状态值加 1
int nextc = c + acquires;
// int类型是有上限的,这里主要是防止可重入次数溢出。
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 重新设置 AQS状态值并返回。
setState(nextc);
return true;
}
return false;
}
以上是非公平锁的实现方式,另外试想一下,如果当前锁被线程A所持有,AQS同步队列中有一个线程B正在等待锁,正常的情况是A释放锁,然后B通过CAS的方式正常获取锁,但是如果B获取锁的时,恰好有一个线程C也通过CAS的方式获取锁,并且成功获取锁了,那么B将又被方法 AQS同步队列,继续挂起。这样对于B而y言是不公平的,因为它等待的时间很久,反而被C“插队”了。
接下来我们看一下公平锁是如何实现的:
// 以公平的方式获取锁
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = this.getState();
if (c == 0) {
// 差异主要在这里,多了一个 hasQueuedPredecessors 判断。
if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, acquires)) {
this.setExclusiveOwnerThread(current);
return true;
}
} else if (current == this.getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
this.setState(nextc);
return true;
}
return false;
}
由此我们可以看出,公平锁与非公平锁的差异是在于公平锁多了一个 !this.hasQueuedPredecessors()判断,我们可以进入该判断看一下:
public final boolean hasQueuedPredecessors() {
Node h, s;
// 如过 AQS同步队列的前驱节点(head)不是空
if ((h = head) != null) {
// 根据前驱节点(head)获取下一个节点,也就是 AQS同步队列中的第一个节点,然后判断其是否为空。
// 如果 h.next不是空,说明 AQS同步队列中已经有线程等待获取锁中。那么就接着判断该节点的线程是否调用了条件变量的 await方法将自己挂起。
if ((s = h.next) == null || s.waitStatus > 0) {
s = null;
// 遍历 AQS同步队列,直到找到一个未调用条件变量的 await方法将自己挂起的线程。
for (Node p = tail; p != h && p != null; p = p.prev) {
if (p.waitStatus <= 0)
s = p;
}
}
// 如果 AQS同步队列中存在一个未调用过 await的线程,并且这个线程并非是当前线程,则返回true。
if (s != null && s.thread != Thread.currentThread())
return true;
}
return false;
}
由此方法便可以看出,如果 AQS同步队列中存在可以正常获取锁的线程,那么新来的线程就无法“插队”。
接下来我们便进入如果调用tryAcquire方法失败,而进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的方法。进入addWaiter方法:
private Node addWaiter(Node mode) {
//创建一个类型为 Node.EXCLUSIVE的Node节点
Node node = new Node(mode);
// 将其加入 AQS同步队列的尾部。
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
// 初始化同步队列,在这个方法中,是创建一个Node节点为哨兵节点,并设置为 AQS前驱节点(head)。
initializeSyncQueue();
}
}
}
接下来我们便进入acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
// 循环的方式获取锁
for (;;) {
// 获取当前线程的前置节点
final Node p = node.predecessor();
// 如果当前线程的前一个(pre)节点就是 AQS的前驱节点(head),那么说明“轮到”这个线程去获取锁了
// 然后调用 tryAcquire方法获取锁
if (p == head && tryAcquire(arg)) {
// 将 AQS的前驱节点(head)设置为当前节点,可以看出这是懒加载的形式。
setHead(node);
p.next = null;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
由此,线程便成功获取锁了。
2.3 释放锁
释放锁就相对而言毕竟容易了,下面我们看一下代码:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 具体逻辑由 AQS子类实现
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
// 将 AQS状态值减去 1(默认为1)
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
// 如果当期线程不是锁的拥有者,就会报错,所以在未获得锁之前,切勿调用锁的 unlock方法
throw new IllegalMonitorStateException();
boolean free = false;
// 如果状态值减去1后为0,则可以释放锁。
if (c == 0) {
free = true;
// 将当前锁的拥有者设置为空
setExclusiveOwnerThread(null);
}
// 设置 AQS状态值
setState(c);
return free;
}
这里需要注意的是,因为是可重入锁,同一线程每获取锁一次状态值就会加1,所以解锁也应该解同样的次数,否则仅仅是状态值减1,不会释放锁。
参考
- 电子工业出版社,翟陆续,薛宾田著,《Java并发编程之美》。
- RedSpider社区,《深入浅出Java多线程》。