JUC源码学习02重入锁(ReentrantLock)学习
Posted 春秋_XH
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC源码学习02重入锁(ReentrantLock)学习相关的知识,希望对你有一定的参考价值。
重入锁(ReentrantLock)学习
重入锁:也是排他锁:即在同一时刻只允许一个线程进行访问。
一、公平和非公平获取锁的区别
1、公平获取与非公平获取两者对比
公平锁:每次都是从同步队列中的第一个节点获取到锁
非公平性锁:每个线程可以连续多次获取锁
TODO: 缺图
非公平锁缺点:
-
非公平锁,可能使线程“饥饿”,饥饿出现的情况。
-
饥饿的原因:当一个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率非常大,使得其他线程只能在同步队列中等待。
2、为什么 ReentrankLock 默认是非公平锁实现?
如果把每次线程获取到锁定义为 1 次切换,公平性锁每次都要进行切换,而非公平性锁可以相比较而言减少线程切换。
TODO:缺图
通过实验对比结论:
在测试中,公平性锁与非公平性锁比较,总耗时是其 94.3 倍,总切换次数是其 103 倍。
可见:
-
公平性锁保证了锁的获取按照 FIFO 原则,而代价是进行了大量的线程切换。
-
非公平性锁虽然造成了线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
二、非公平锁的获取和释放
以非公平锁的获取和释放为例。
1、tryAcquire(int acquires)
java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
static final class NonfairSync extends Sync
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock()
// 非公平锁的获取:只要 CAS 设置同步状态成功,则表示当前线程获取了锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
// 非公平锁的获取
protected final boolean tryAcquire(int acquires)
return nonfairTryAcquire(acquires);
上面代码可以看到:非公平锁的获取,只要 CAS 设置同步状态成功,则表示当前线程获取了锁
1.1 nonfairTryAcquire(int acquires)
final boolean nonfairTryAcquire(int acquires)
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0)
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;
该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true。表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值。
这也就要求 ReentrantLock 在释放同步状态时减少同步状态值。
2、 tryRelease(int releases)
protected final boolean tryRelease(int releases)
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0)
// 最终释放条件:同步状态是否为 0
free = true;
// 将占有线程设置为 null
setExclusiveOwnerThread(null);
setState(c);
return free;
如果该锁被释放了 n 次,那么前(n-1)次 tryRelease(int releases)方法必须返回 flase,而只有同步状态完全释放了,才能返回 true。可以看到,该方法将同步状态是否为 0 作为最终释放的条件,当同步状态为0 时,将占有线程设置为 null,并返回 true,表示释放成功。
三、公平锁的获取与释放
公平和非公平,是针对锁而言的
如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。
下面有代码中有标记其区别:
1、tryAcquire(int acquires)方法
static final class FairSync extends Sync
private static final long serialVersionUID = -3000897897090466540L;
final void lock()
acquire(1);
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires)
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0)
// 公平锁的获取与非公平锁区别:hasQueuedPredecessors()
// 判断条件多了方法:加入了同步队列中当前节点是否有前驱节点的判断
// 如果该方法返回 true,表示有线程比当前线程更早的请求获取锁,
// 因此需要等待前驱线程获取锁并释放锁之后才能继续获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires))
// cas 成功。
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;
2、hasQueuedPredecessors()
公平锁中加入的一个判断条件:加入了同步队列中当前节点是否有前驱节点的判断
public final boolean hasQueuedPredecessors()
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
ReentrantLock (重入锁) 源码浅析
一、ReentrantLock简介ReentrantLock重入锁,顾名思义,就是支持重入的锁,它表示能够支持一个线程对资源的重复加锁;我们之前学习过Synchronized锁,它也是支持重入的一种锁,参考我的另一篇Synchronized 锁的实现原理与应用,Synchronized支持隐式的重入锁,比如递归方法,在方法运行时,执行线程在获取到了锁之后仍能连续多次地获取锁;ReentrantLock虽然不能隐式重入,但是获取到锁的线程多次调用lock方法,不会阻塞进入同步队列;除此之外在获取锁时支持公平或者非公平的选择。
二、主要成员和结构图
①、ReentrantLock关系图
②、Sync是ReentrantLock的内部类,继承AQS
③、FairSync公平的锁实现,也是ReentrantLock的内部类,继承Sync
④、NonfairSync非公平的锁实现,也是ReentrantLock的内部类,继承Sync
三、主要的方法
分析一些常用方法,不会介绍AQS,AQS的一些方法参考我的这一篇文章
①、构造方法,我们可以看出默认的无参是非公平锁,有参构造true表示公平,false表示非公平。
// 无参
public ReentrantLock() {
sync = new NonfairSync();
}
// 有参
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
②、lock()获取锁,其实就是把state从0变成n(重入锁可以累加)。
实际调用的是sync的lock方法,分公平和非公平。
public void lock() {
sync.lock();
}
公平实现:FairSync,我们发现其实调用的是acquire,其实这个是AQS的acquire,然后aqs的acquire的方法里面又会调用tryAcquire方法,因为这个方法需要同步组件自己去实现,所以ReentrantLock里面重写了AQS的tryAcquire方法,所以我们获取到锁就会返回true,没有就会返回false;然后没有获取到锁的线程就交给AQS去处理。
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don‘t grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
// 获取当前的线程
final Thread current = Thread.currentThread();
// 获取锁的状态
int c = getState();
if (c == 0) {
// hasQueuedPredecessors 判断队列还有没有其它node,要保证公平
// 没有在用cas设置状态
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;
}
}
非公平实现:NonfairSync,我们可以发现基本和公平一样,就没有hasQueuedPredecessors方法,没有遵循FIFO队列的模式,而是不管队列有没有node,自己都可以去获取锁,不需要排队
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
②、lockInterruptibly支持中断的获取锁,其实是调用了AQS的lockInterruptibly方法,在AQS方法里面又回去调用tryAcquire方法,这个方法在上面已经解释过了。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
AQS的lockInterruptibly方法
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
③、tryLock(long timeout, TimeUnit unit),支持中断,并且在这个基础上增加了超时设置,其实也是调用了AQS的tryAcquireNanos方法,我们发现其实他也是调用的tryAcquire方法。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
AQS的tryAcquireNanos方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
④、unlock释放锁,其实就是把state从n(可能发生了锁的重入,需要多次释放)变成0,这个不区分公平与非公平,首先其实也是调用AQS的release方法,然后AQS在调用子类Sync的tryRelease方法。
public void unlock() {
sync.release(1);
}
调用Sync的tryRelease方法
protected final boolean tryRelease(int releases) {
// 获取锁的状态
int c = getState() - releases;
// 获得锁的线程才能释放锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 直到锁的状态是0,说明锁释放成功,因为有重入锁
// 说明我们在一个线程里面调用几次lock,就要调用几次unlock,才能最终释放锁
if (c == 0) {
free = true;
// 释放线程的拥有者
setExclusiveOwnerThread(null);
}
// 设置锁的状态
setState(c);
return free;
}
⑤、newCondition方法,创建一个newCondition。
public Condition newCondition() {
return sync.newCondition();
}
⑥、getHoldCount方法,获取当前线程获得锁的个数。
public int getHoldCount() {
return sync.getHoldCount();
}
final int getHoldCount() {
// 当前线程是否获取到锁
return isHeldExclusively() ? getState() : 0;
}
⑦、isHeldByCurrentThread方法,当前线程是否获取到锁。
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don‘t need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
⑧、isLocked方法,是否有线程获取到了锁。
final boolean isLocked() {
return getState() != 0;
}
⑨、getOwner方法,获取取得锁的线程。
⑩、getQueueLength方法,获取同步队列的数量。
public final int getQueueLength() {
// 从aqs的尾节点开始往前遍历,除去空节点(但是其实只有第一个节点是空节点),也就是thread != null
int n = 0;
for (Node p = tail; p != null; p = p.prev) {
if (p.thread != null)
++n;
}
return n;
}
四、总结
学习ReentrantLock,我们主要需要了解它,公平和非公平的实现,以及重入锁的获取与释放的流程,还有最重要的就是要了解AQS,这是实现重入锁的基础,因为ReentrantLock只是实现了AQS获取锁和释放锁制定的模板方法的语义,所以要理解ReentrantLock获取锁成功和失败具体都做了什么逻辑,和AQS的实现是离不开的。
可以参考我的这一篇AQS的文章。
参考 《Java 并发编程的艺术》
以上是关于JUC源码学习02重入锁(ReentrantLock)学习的主要内容,如果未能解决你的问题,请参考以下文章
JUC并发编程 JUC AQS原理 -- AQS概述 & 实现不可重入锁
java--JUC--公平锁,非公平锁,可重入锁,自旋锁,死锁
并发编程-concurrent指南-Lock-可重入锁(ReentrantLock)
JUC并发编程 -- ReentrantLock可重入锁(可重入 & 可打断 & 锁超时 & 锁超时-解决哲学家就餐)
JUC并发编程 共享模式之工具 JUC 读写锁 ReentrantReadWriteLock -- ReentrantReadWriteLock(不可重入锁)使用 & 注意事项