一、ReentrantLock 介绍
1、什么是 ReentrantLock?
ReentrantLock是juc包下提供的一种锁,他是基于AQS来实现的;
ReentrantLock既可以创建公平锁,也可以创建非公平锁,在ReentrantLock 内部分别定义了
公平锁和非公平锁的内部类:FairSync(公平锁)、NonfairSync(非公平锁)
2、ReentrantLock 的构造方法
ReentrantLock 中没有值得关注的属性,但有2个构造方法需要我们关注下,如下所示:
由上边2个构造函数可以发现ReentrantLock 默认实现的是非公平锁(非公平锁性能比较高)
若要创建公平锁 ReetrantLock,可以使用他的有参构造函数,并传入参数true
,如:new ReetrantLock(true) 创建的是公平锁的ReetrantLock。
二、ReentrantLock 与 Synchronized 之间的区别
1、ReentrantLock是一个类,而 Synchronized 是一个关键字,他们都是在JVM层面实现
互斥锁的方式
2、如果线程竞争比较激烈的场景中,推荐使用 ReentrantLock,因为 Synchronized 有个
锁升级的过程,若Synchronized处于 “轻量级锁 或 偏向锁”状态时,性能还是可以的,若
Synchronized 处于 “重量级” 锁时,性能就比较低了,而且Synchronized 不存在锁降级
,一旦升级到重量级锁,之后 Synchronized 一直是重量级锁。
3、实现原理不同,ReentrantLock基于AQS实现的,Synchronized 基于ObjectMonitor实现的
4、ReentrantLock 功能比 Synchronized 更全面,如:
1)ReentrantLock 可以实现公平锁,也可以实现非公平锁;而 Synchronized只能实现
非公平锁。
2)ReentrantLock可以指定锁支援的等待时间
5、使用方式不同,ReentrantLock加锁之后需要手动释放锁,而Synchronized 可以自动
释放锁。
三、加锁流程
1、ReentrantLock 非公平锁加锁流程
AQS内部类Node 的属性 waitStatus 用来表示 锁线程 的状态,waitStatus初始值为0;
哪个线程通过CAS将 waitStatus 从0改为1,即表示竞争到了锁(即加锁成功)。
若 waitStatus=-1,表示当前节点的下一个节点的线程是挂起的
公平锁的加锁流程与非公平锁的差不多,公平锁不需要进来就CAS竞争锁,而是直接进入AQS
阻塞队列去排队。
四、加锁源码解析
1、lock方法
ReetrantLock 类中,lock() 是一个接口,它的是由2个子类 FairSync 和 NonFairSync
实现的;
/**
非公平锁的加锁方法
*/
final void lock() {
//非公平锁直接抢锁,不管有没有线程在排队
//同步状态为0:表示没有线程持有锁,
// 若锁状态能从0修改成1表示当前线程抢到了锁,并设置当前线程持有锁
if (compareAndSetState(0, 1))
//设置当前线程持有锁
setExclusiveOwnerThread(Thread.currentThread());
else//若当前线程已经持有锁,则同步状态加1,即锁的个数加1(这个过程称为锁重入)
//锁重入,这个方法最终执行的是下面的方法 tryAcquire
acquire(1);//枪锁失败,进入AQS标准枪锁流程(即当前线程放入竞争锁队列)
}
/**
* 加锁,lock.lock() 方法
* 每次加锁参数都是1
公平锁的加锁方法
*/
final void lock() {
//没有尝试抢锁,直接进入AQS标准抢锁流程
//调用AQS中的方法(请参考前边的AQS笔记)
acquire(1);
}
2、tryAcquire 方法
tryAcquire方法的功能是尝试获取锁资源,若获取成功返回true,获取失败返回false;
tryAcquire 定义在 AQS中,但是由各个子类来实现的。在ReetrantLock中 tryAcquire 是
分别在内部类 FairSync(公平锁)和 NonfairSync(非公平锁)实现的。
1)tryAcquire 在内部类 FairSync(公平锁)中的实现
/**
* 公平锁的方式加锁
* AQS调用,子类自己实现获取锁的流程
*/
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前同步(锁)状态
int c = getState();
//c==0表示当前其他持有锁的线程释放了锁,当前情况是锁没有被线程持有
if (c == 0) {
/*
* hasQueuedPredecessors方法是看下队列中有没有其他线程在排队,即锁没有被持有的情况下,是否存在有其他线程在排队,
* 若不存在比当前线程等待时间更长的线程(即当前线程在AQS阻塞队列头部),且更改同步状态成功,则设置当前线程持有锁,并返回true;
* 否则获取锁失败,返回false
*
* todo 公平锁与非公平锁的区别?
* 公平锁下,当当前线程去竞争锁时,若锁没有被其他线程持有,则先判断同步器(AbstractQueuedSynchronizer)中
* 的阻塞队列中有没有比当前线程 “等待时间更长的线程”,若有则不回获取锁,把锁让给等待时间最长的线程,所以锁的
* 获取是公平的;
* 非公平锁下,只要锁没有被其他线程持有,则当前线程立即获取锁,不管它是不是等待时间最长的线程
*
*/
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;
}
}
/**
* 判断 是否存在比当前线程等待时间长的线程或节点(节点关联线程)
* AQS的方法
*/
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order 按反向初始化顺序读取节点
Node h = head;
Node s;
//若队列不为空,且队列中除了头节点外第一个节点中关联的线程不是当前线程,
//则返回true
//todo 在并发条件下,在判断 h != t 成立后,后面 s = h.next 可能为空,这种情况下也认为存在有比当前线程等待时间长的线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());//todo 注意:在队列不为空的情况下,s一定不为空,
}
2)tryAcquire 在内部类 NonfairSync(非公平锁)中的实现
tryAcquire在 NonfairSync的实现很简单,主要调用是内部类Sync的
方法 nonfairTryAcquire
//查看当前线程是否能获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);//非公平锁标准获取流程
}
3)nonfairTryAcquire(int acquires) 方法
nonfairTryAcquire 是ReetrantLock内部类Sync的方法,主要是用来实现非公平锁获取
锁的过程
nonfairTryAcquire 方法代码如下:
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前的同步状态(即锁是否被持有)
int c = getState();
//执行到这里时,正好其他持有锁的线程释放了锁,可以尝试枪锁,
//c==0,表示锁没有被持有
if (c == 0) {//锁没有线程持有
//继续枪锁,不看有没有线程排队
if (compareAndSetState(0, acquires)) {//枪锁,并修改同步状态,即表示设置锁被线程持有
//枪锁成功,则设置锁被当前线程持有
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//若当前线程就是持有锁的线程,则锁重入
//设置新的同步状态,即锁重入,
//同步状态即是加锁的次数
//使用 state 记录锁重入次数
int nextc = c + acquires;
// 超过了int类型范围,表明符号溢出,所以抛出异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//将重入次数赋值给变量state
setState(nextc);
return true;
}
//获取锁失败,表明当前线程通过AQS将当前线程放入阻塞队列,然后进行阻塞操作等待线程被唤醒获取锁
return false;
}
3、tryLock 方法
我们需要分析无参数的 tryLock() 和 带超时时间的 tryLock(long timeout, TimeUnit unit)
1)tryLock() 方法
该很简单,他调用 ReetrantLock内部类Sync的nonfairTryAcquire 方法
去获取锁,若获取锁成功则返回true,获取锁失败返回false
tryLock() 代码如下:
方法 nonfairTryAcquire 的解析在上边。
2)tryLock(long timeout, TimeUnit unit)
该方法功能也是尝试获取锁,与tryLock()的区别是:若等待时间超过 timeout,则直接
返回fase,表示获取锁失败。
tryLock(long timeout, TimeUnit unit) 结构如下:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
/**
* 独占式的同步状态获取(即锁的获取)(互斥锁),可响应中断,并具有超时控制;
* 线程在队列中 阻塞和非阻塞 交替变换的结束条件为:同步状态获取成功、被中断、超时
* 这三种情况
*
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())//被中断,则抛出异常
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout); //
}
/**
* Acquires in exclusive timed mode.
*
* (当前线程)采用独占计时模式获取锁
*
* @param arg the acquire argument acquire 方法参数
* @param nanosTimeout max wait time 最大等待时间,单位纳秒
* @return {@code true} if acquired
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//若等待时间小于等于0,同步状态获取失败
if (nanosTimeout <= 0L)
return false;
//计算同步状态的结束时间点 System.nanoTime():返回当前时间的纳秒数
/*
* 针对超时控制,程序首先计算超时的截止时间点deadline,deadline = System.nanoTime() + nanosTimeout。
* 如果获取同步状态失败,则需要重新计算休眠的时间间隔nanosTimeout = deadline - System.nanoTime(),
* 如果nanosTimeout <= 0 表示已经超时了,返回false,如果nanosTimeout > spinForTimeoutThreshold(1000纳秒),
* 使当前线程等待nanosTimeout纳秒,否则,如果nanosTimeout <= spinForTimeoutThreshold,那该线程将不会超时等待,
* 而是进入快速的自旋过程。原因在于:非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的
* 超时从整体上表现得反而不精确。因此,在超时非常短的情况下,同步器会进入无条件的快速自旋。
*/
final long deadline = System.nanoTime() + nanosTimeout;
//采用独占模式 Node.EXCLUSIVE 为当前线程创建一个节点,并把节点添加到队列中,返回新创建的节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//死循环,是一个“自旋”过程,自旋获取同步状态
for (;;) {
final Node p = node.predecessor();
//只有node 的前置节点为头节点,当前线程才能够获取锁成功
//若当前线程成功了获取锁,则退出自循环
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//重新计算超时时间间隔
nanosTimeout = deadline - System.nanoTime();
//若超时时间小于等于0,则获取同步状态失败
if (nanosTimeout <= 0L)
return false;
//若节点node获取节点失败且 剩余时间 nanosTimeout > spinForTimeoutThreshold),
// 则当前线程阻塞,继续等待,
//若当前线程获取锁失败要接入阻塞状态
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())//若当前线程在阻塞状态下被中断,则抛出异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
注意:tryAcquire方法请参考上边 1
4、lockInterruptibly()方法
该方法的功能也是获取锁资源,但在获取锁资源之前会先检查当前线程线程是否被中断,
若当前线程已经被中断,则抛出中断异常退出。
注意:
该方法在获取锁资源失败时,会一直等待,直到当前线程被唤醒或被中断;
若被中断会异常退出。
lockInterruptibly 方法代码如下:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/*
* 独占式的同步状态获取(即锁的获取),响应中断,即该在等待获取同步状态时,若
* 当前线程被中断了,则立即抛出 InterruptedException 异常
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())//首先检查中断状态
throw new InterruptedException();
/*
* 当前线程尝试获取锁,若成功了获取锁,则直接结束,否则执行方法 doAcquireInterruptibly(arg) ,先
* 把当前线程入队列,然后在 阻塞和非阻塞 之间连续变换,直到 tryAcquire() 方法执行成功为止
*/
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
/**
* 同步可中断模式下同步状态(即锁)的获取操作
*/
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//采用给定的模式,为当前线程创建一个节点,并把节点添加到队列中,返回新创建的节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//死循环,是一个 “自循环” 的过程
for (;;) {
//获取 node 的前置节点
final Node p = node.predecessor();
//只有node 的前置节点为头节点,当前线程才能够获取锁成功
//若当前线程成功了获取锁,则退出自循环
if (p == head && tryAcquire(arg)) {
//设置头节点
setHead(node);
//删除原先的头节点
p.next = null; // help GC
failed = false;
return;
}
//若当前线程获取锁失败,且当前线程已经阻塞,若中断阻塞状态下的当前线程,则直接抛出异常
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* 判断当前线程阻塞方式:park 或 interrupted
* 根据中断方式可以知道唤醒当前阻塞线程的方式
*/
private final boolean parkAndCheckInterrupt() {
/**
* 这里的this指代当前AQS对象,表示当前线程阻塞在那个对象上,后期可以通过jstack来看到,用于排查问题
*/
LockSupport.park(this);//阻塞当前线程
//判断是否以中断的方式来唤醒当前阻塞(挂起)的线程,还是以正常方式唤醒当前(挂起)线程
//若是中断唤醒,则返回true,否则返回false
//唤醒线程方式:1、interrupt,2、unpark
return Thread.interrupted();//
}
五、ReetrantLock释放锁的过程
线程调用ReetrantLock的unlock() 方法释放锁时,就是调用tryRelease()方法去释放锁;
释放锁时,首先判断当前线程是否持有锁资源,若当前线程没有持有锁资源,则直接抛出
异常。
tryRelease() 方法的参数 releases 表示释放锁的次数(该参数一般默认为1),当释放锁
时,会拿当前线程状态 state - releases,若结果为0,表示释放锁成功,否则释放锁失败;
释放锁成功后唤醒AQS中的后继有效的节点。
ReetrantLock 释放锁代码如下:
/**
ReetrantLock 方法
*/
public void unlock() {
sync.release(1);
}
/**
* 独占式同步状态释放(即锁释放)
*
* 若tryRelease() 方法返回true,即当前线程成功了释放同步状态,则会通过unparkSuccessor(Node)方法
* 唤醒头节点的后继节点线程,该方法最终调用了LockSupport.unpark(Thread)方法来唤醒处于等待状态线程
*/
public final boolean release(int arg) {
//释放写锁
if (tryRelease(arg)) {//子类判定释放锁是否成功
//子类释放锁成功后成,检查阻塞队列唤醒即可
Node h = head;
//若头节点不为null,且头节点未被取消(即头节点的状态不为0),则调用unparkSuccessor() 方法唤醒
//头节点的后序节点,然后返回true
if (h != null && //若头节点h=null,表示AQS队列从来没有被使用过
h.waitStatus != 0) //头节点若没有被取消,即状态值不为0,那么头节点状态值一定是SIGNAL=-1,是活跃的节点
//头节点是有效的节点,且标注需要唤醒后继节点,那么唤醒即可
unparkSuccessor(h);
return true;
}
return false;
}
/**
* 释放锁,公平锁与非公平锁公用方法,释放锁并不区分是否是非公平锁
* 内部类 Sync 的方法
*/
protected final boolean tryRelease(int releases) {
//计算新的同步状态,即锁的状态
int c = getState() - releases;
//若持有锁的线程不是当前线程,即当前线程没有持有锁,则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//锁释放标志
boolean free = false;
//若新的状态c 为0,则表示没有线程持有锁,即释放锁成功
//并设置线程持有者为null,即没有线程持有锁,锁释放成功了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//重入锁,设置新的同步状态(锁状态)
//注意:此时全局变量state 的值没有改变,也即表明在setState之前,没有别的线程能够获取锁
//这时保证了上面 if 块的原子性
setState(c);
//free=true 表示锁释放成功了,AQS可以唤醒阻塞队列中正在等待的线程
return free;
}