ReentrantLock实现关系
Lock接口
在 Lock 接口出现之前,Java中的应用程序对于多线程的并发安全处理只能基于 synchronized 关键字来解决,但是 synchronized 他不灵活。Lock 的出现可以解决 synchronized在某些场景中的短板,它比synchronized更加灵活。
Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。
- 实现Lock接口的类有很多,以下为几个常见的锁实现
- ReentrantLock:表示重入锁,它是唯一一个实现了Lock接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数
- ReentrantReadWriteLock:重入读写锁,它实现了ReadWriteLock接口,在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
- StampedLock:stampedLock是JDK8引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。 stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。
Lock的接口方法
- void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
- void lockInterruptibly() // 和 lock()方法相似, 但阻塞的线程可中断,抛出 java.lang.InterruptedException异常
- boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回true
- boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
- void unlock() // 释放锁
sync
- 锁的同步控制的基础,也就是说获取锁的方法。
- 公平锁:FairSync
- 非公平锁:NonfairSync
ReentrantLock重入锁详解
重入锁,表示支持重新进入的锁,也就是说,如果当前线程t1通过调用lock方法获取了锁之后,再次调用lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。synchronized和ReentrantLock都是可重入锁。
- 代码演示一下什么是重入锁
- 我们假设 synchronized 不支持重入锁,那么一定会成为死锁。因为主线程调用demo()方法,已经拿到了该对象锁,而demo()中又调用了demo2()方法,而demo2()中又得拿该对象锁,而此时该锁已经被拿走了,所以demo2()方法等着demo()方法结束,他才能拿锁,而demo()方法又要执行完demo2()方法才能释放锁。现在死锁了!!!!
- 因为 synchronized 支持重入锁所以他不会发生上面描述的问题。
ReentrantLock 的实现原理
我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在 synchronized 中,我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。
那么在ReentrantLock中,也一定会存在这样的需要去解决的问题。就是在多线程竞争重入锁时,竞争失败的线程如何实现阻塞以及如何被唤醒的?
AQS
在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它 是一个同步工具也是Lock用来实现线程同步的核心组件。如果你搞懂了AQS,那么J.U.C中绝大部分的工具都能轻松掌握。
- 可以看到 ReentrantLock 中的Sync属性继承了AQS接口,也就是用来实现线程的同步执行的方法实际上由AQS来完成。
AQS 的内部实现
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成Node加入到ASQ队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
Node的组成
AQS 的两种功能
从使用层面来说,AQS的功能分为两种:独占和共享
- 独占锁:每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是以独占方式实现的互斥锁。
- 共享锁:允许多个线程同时获取锁,并发访问共享资源,比如 ReentrantReadWriteLock。
ReentrantLock 的源码分析
- 以ReentrantLock作为切入点,来看看在这个场景中是如何使用AQS来实现线程的同步的
ReentrantLock 的时序图
- 调用ReentrantLock中的lock()方法,源码的调用过程我使用了时序图来展现。
- 这个是 ReentrantLock 获取锁的入口
public void lock() {
sync.lock();
}
sync实际上是一个抽象的静态内部类,它继承了AQS来实现重入锁的逻辑,我们前面说过AQS是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承AQS来实现对应场景的功能。
Sync有两个具体的实现类
- NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
- FailSync: 表示所有线程严格按照FIFO来获取锁
- 以非公平锁为例,来看看lock中的实现
- 非公平锁和公平锁最大的区别在于,在非公平锁中我抢占锁的逻辑是,不管有 没有线程排队,我先上来cas去抢占一下
- CAS成功,就表示成功获得了锁
- CAS失败,调用acquire(1)走锁竞争逻辑
final void lock() {
// 第一次试图插队,如果返回 true 说明此时锁为空
if (compareAndSetState(0, 1))
//设置属性,表示该线程拿到了锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
- CAS实现原理
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的 state 的值和预期值 expect 相等,则替换为update。更新成功返回true,否则返 回 false 这个操作是原子的,不会出现线程安全问题,这里面涉及到 Unsafe 这个类的操作,以及涉及到state这个属性的意义。
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示
- 当 state = 0时,表示无锁状态
- 当 state > 0 时,表示已经有线程获得了锁,也就是 state = 1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增,比如重入5次,那么state=5。而在释放锁的时候,同样需要释放5次直到 state = 0 其他线程才有资格获得锁。
- 了解一下CAS 的 C++ 源码
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset,jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj); //将Java对象解析成JVM的oop(普通对象指针), jint* addr = (jint *)
index_oop_from_field_offset_long(p, offset); //根据对象p和地址偏移量找到地址
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //基于cas比较并替换, x表示需要更新的值,addr表示state 在内存中的地址,e表示预期值 UNSAFE_END
- 如果成功则设置属性
protected final void setExclusiveOwnerThread(Thread thread) {
//exclusiveOwnerThread 属性设置为当前线程
//exclusiveOwnerThread:独占模式同步的当前所有者
exclusiveOwnerThread = thread;
}
- 如果失败则进入 accquire 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此时继续acquire(1)操作
- 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
- 如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部
- acquireQueued,将Node作为参数,通过自旋去尝试获取锁。
- tryAcquire() 是一个钩子方法。这个方法的作用是尝试获取锁,如果成功返回true,不成功返回 false 它是重写 AQS 类中的 tryAcquire 方法,
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
- nonfairTryAcquire() 方法
- 获取当前线程,判断当前的锁的状态
- 如果state=0表示当前是无锁状态,通过cas更新state状态的值
- 当前线程是属于重入,则增加重入次数
final boolean nonfairTryAcquire(int acquires) {
//拿到当前执行的线程
final Thread current = Thread.currentThread();
//拿到 state 的值,如果锁此时不被占用返回0,被占用返回不为0
int c = getState();
//判断state是否位0
if (c == 0) {
//第二次试图插队,acquires的值为1
if (compareAndSetState(0, acquires)) {
//当前线程拿到锁
setExclusiveOwnerThread(current);
//返回true
return true;
}
}
//如果当前线程已经拿到锁了,并且又尝试获取锁
else if (current == getExclusiveOwnerThread()) {
//state的值+1,代表重入次数
int nextc = c + acquires;
//如果 < 0(不理解什么场景会出现),抛出异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//更新state值
setState(nextc);
//返回true
return true;
}
//没拿到锁返回 false
return false;
}
当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成 Node 入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了AQS的独占锁功能。
- 将当前线程封装成Node。
- 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的 node添加到AQS队列。
- 如果为空或者cas失败,调用enq将节点添加到AQS队列。
private Node addWaiter(Node mode) {
//把当前线程封装成一个Node节点
Node node = new Node(Thread.currentThread(), mode);
//tail 是 AQS 中表示同比队列队尾的属性,默认是 null
Node pred = tail;
//如果尾节点不为null
if (pred != null) {
//设置该Node节点的前置节点为尾节点
node.prev = pred;
//通过 cas 把 node 加入到 AQS 队列,也就是设置为 tail
if (compareAndSetTail(pred, node)) {
//此时的尾节点已经改变了
//设置上一个尾节点的下一个节点为该Node节点
pred.next = node;
//返回该线程节点
return node;
}
}
//如果为null,也就是线程第一次想加入 AQS 队列,对 AQS 队列进行初始化
enq(node);
//返回当前线程的节点
return node;
}
// enq 就是通过自旋操作把当前节点加入到队列中
private Node enq(final Node node) {
//自旋操作
for (;;) {
//拿到尾节点
Node t = tail;
//如果尾节点为null
if (t == null) { // Must initialize
//通过 CAS 操作创建一个Node节点并赋给head属性
if (compareAndSetHead(new Node()))
//尾节点设置为head(头)节点,此时头尾属性都指向同一个节点
tail = head;
} else {
//尾节点不为null,当前线程的Node节点的前置节点指向头节点
node.prev = t;
//通过 CAS 操作把当前节点添加进 AQS 队列
if (compareAndSetTail(t, node)) {
//头节点的下一个节点指向当前线程的Node节点
t.next = node;
//返回的是头节点,也就是那个创建的空节点
return t;
}
}
}
}
图解分析
假设3个线程来争抢锁,那么截止到enq方法运行结束之后,或者调用addwaiter方法结束后,AQS中的链表结构图
AQS.acquireQueued 通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给 acquireQueued 方法,去竞争锁
- 获取当前节点的prev节点
- 如果prev节点为head节点,那么它就有资格去争抢锁,调用tryAcquire抢占锁
- 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head 节点
- 如果获得锁失败,则根据waitStatus决定是否需要挂起线程
- 最后,通过cancelAcquire取消获得锁的操作
final boolean acquireQueued(final Node node, int arg) {
//设置 failed 标识
boolean failed = true;
try {
//设置 interrupted 标识
boolean interrupted = false;
//自旋操作,此时所有的等待线程都会阻塞在这里
for (;;) {
//拿到该线程的Node节点的 prev 指向的节点
final Node p = node.predecessor();
//如果p是头节点 && 拿到锁成功
if (p == head && tryAcquire(arg)) {
//获取锁成功,则把头节点的下一个节点设置为头节点
setHead(node);
//把原 head 节点从链表中移除
p.next = null; // help GC
failed = false;
//返回false,结束自旋
return interrupted;
}
//给线程设置一个标识
if (shouldParkAfterFailedAcquire(p, node) &&
//判断是否有过中断操作
parkAndCheckInterrupt())
//并且设置当前线程的 interrupted 属性为 true,代表不能被中断
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire
如果ThreadA的锁还没有释放的情况下,ThreadB和ThreadC来争抢锁肯定是会失败,那么失败以后会调用shouldParkAfterFailedAcquire方法 Node 有5种状态
- CANCELLED:在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点, 其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
- SIGNAL:只要前置节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程
- CONDITION:和Condition有关系,
- PROPAGATE:共享模式下,PROPAGATE状态的线程处于可运行状态
- 默认状态:
这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是否应该被挂起。
- 如果ThreadA的pred节点状态为SIGNAL,那就表示可以放心挂起当前线程
- 通过循环扫描链表把CANCELLED状态的节点移除
- 修改pred节点的状态为SIGNAL,返回false. 返回false时,也就是不需要挂起,返回true,则需要调用 parkAndCheckInterrupt 挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//拿到前置节点的 waitStatus
int ws = pred.waitStatus;
//如果前置节点为SIGNAL状态,返回true
if (ws == Node.SIGNAL)
return true;
//ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行
if (ws > 0) {
do {
node.prev = pred = pred.prev;
//相当于: pred=pred.prev; node.prev=pred;
} while (pred.waitStatus > 0);
//这里采用循环,从双向列表中移除 CANCELLED 的节点
pred.next = node;
} else {
//利用 cas 设置 prev 节点的状态为 SIGNAL(1)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回 false
return false;
}
parkAndCheckInterrupt
使用LockSupport.park挂起当前线程编程WATING状态 Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是 thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识 true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回true,意味着在acquire方法中会执行selfInterrupt()。
private final boolean parkAndCheckInterrupt() {
//把当前线程阻塞,简单理解成wait()方法即可,所有的线程都被阻塞在此处
LockSupport.park(this);
//返回中断标识
return Thread.interrupted();
}
- 图解分析
通过acquireQueued方法来竞争锁,如果ThreadA还在执行中没有释放锁的话,意味着ThreadB和ThreadC只能挂起了。
- 最后如果有中断操作则执行 selfInterrupt() 方法。
static void selfInterrupt() {
// 中断线程
Thread.currentThread().interrupt();
}
- 解释一下在 AQS 中断操作的意思。
举个例子:假如A和B在公司上班,A和B需要合作完成一个项目,但是只有一个电脑,那么A和B只能排队使用电脑完成自己的任务,A先去敲代码,B此时没事干就去休假了。但是公司想要把B辞退,现在辞退不了啊,因为B去休假了不在公司,所以公司就记录一下辞退B的任务。B休假结束会公司了,公司发现有一个辞退任务,ok,此时再把B辞退。
ReentrantLock 的 unLock() 操作
public void unlock() {
//调用 AQS 的release()方法
sync.release(1);
}
public final boolean release(int arg) {
//如果释放锁成功
if (tryRelease(arg)) {
//拿到此时的头节点
Node h = head;
//如果不为null && 且节点状态 != 0
if (h != null && h.waitStatus != 0)
//调用unparkSuccessor()方法,唤醒某个线程
unparkSuccessor(h);
return true;
}
return false;
}
ReentrantLock.tryRelease
这个方法可以认为是一个设置锁状态的操作,通过将state状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、 3、 4这些值,只有unlock() 的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回true。
protected final boolean tryRelease(int releases) {
//拿到此时 State属性 - 1 的值
int c = getState() - releases;
//如果当前线程不为拿到锁的线程则报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//定义 free 标识
boolean free = false;
//如果为0,标识释放锁成功
if (c == 0) {
free = true;
//设置拿到锁的线程为null
setExclusiveOwnerThread(null);
}
//更新 state 的值
setState(c);
//返回释放锁的结果 true/false
return free;
}
unparkSuccessor
private void unparkSuccessor(Node node) {
//拿到头节点的状态
int ws = node.waitStatus;
if (ws < 0)
//使用 CAS 操作设置头节点的状态为 0
compareAndSetWaitStatus(node, ws, 0);
//拿到头节点的下一个节点
Node s = node.next;
//如果下一个节点为null || 下一个节点的状态 > 0
if (s == null || s.waitStatus > 0) {
s = null;
//通过从尾部节点开始扫描,找到距离 head 最近的一个 waitStatus<=0 的节点
/**
* 为什么从尾部遍历,因为在插入 AQS 队列的时候,先修改的尾节点,在设置上一个尾节点的next指向新的尾节点。
* 如果在这个过程中,发生了锁的释放,有可能会发生next -> null 的情况
*/
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒线程(简单理解为notify()方法)
LockSupport.unpark(s.thread);
}
- 图解分析
- 设置新head节点的prev=null
- 设置原head节点的next节点为null
- 执行被唤醒的线程的方法
最后再简单看一下公平锁
锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序 就应该符合请求的绝对时间顺序,也就是 FIFO。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
//非公平锁在获取锁的时候,会先通过CAS进行抢占,而公平锁则不会。
acquire(1);
}
- 公平锁的 tryAcquire 也有点不同
他判断了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}