ReentrantLock是可重入的互斥锁,即同一时间只有一个线程能够获取锁定资源,执行锁定范围内的代码。具有与synchronized相同功能,但是会比synchronized更加灵活(具有更多的方法)。
ReentrantLock 和 Synchronized的对比
ReentrantLock | Synchronized | |
---|---|---|
锁实现机制 | 依赖AQS | 监视器模式 |
灵活性 | 支持响应中断、超时、尝试获取锁 | 不灵活 |
释放形式 | 必须显示调用unlock()释放锁 | 自动释放监视器 |
锁类型 | 公平锁&非公平锁 | 非公平锁 |
条件队列 | 可关联多个条件队列 | 关联一个条件队列 |
可重入性 | 可重入 | 可重入 |
ReentrantLock结构组成
ReentrantLock实现了Lock接口,Lock接口是Java中对锁操作行为的统一规范,ReentrantLock内部定义了专门的组件Sync, Sync继承AbstractQueuedSynchronizer提供释放资源的实现,NonfairSync和FairSync是基于Sync扩展的子类,即ReentrantLock的非公平模式与公平模式,它们作为Lock接口功能的基本实现。
锁实现
公平锁与非公平锁
获取锁失败的线程,会进入队列阻塞,占有锁的线程在释放锁时会唤醒队列线程去获取锁,但是此时可能还有新的并发线程来获取锁.
这时就会形成新线程和队列线程竞争锁的局面。
- 如果不会因为队列线程已经排了很久的队就把锁让给队列线程,这就是非公平锁,
- 如果为了保证公平一定会让队列线程竞争成功,这就是公平锁,公平锁的性能会差一点
排它锁(x锁):若事务T对数据D加X锁,则其它任何事务都不能再对D加任何类型的锁,直至T释放D上的X锁;一般要求在修改数据前要向该数据加排它锁,所以排它锁又称为写锁。
共享锁(s锁):若事务T对数据D加S锁,则其它事务只能对D加S锁,而不能加x锁,直至T释放D上的S锁;一般要求在读取数据前要向该数据加共享锁,所以共享锁又称为读锁。
ReentrantLock默认为非公平锁,要使用公平锁在构造函数中传入true即可
加锁操作
源码
- 当我们调用ReentrantLock提供的加锁方法lock(图中B处)时,会调用其内部类Sync的抽象方法lock(图中C处)
- Sync.lock()的具体实现是FairSync(公平锁)、NonfairSync(非公平锁)两个子类的lock()方法(图中D1、D2处)
- D1、D2处都会调用Sync的父类AbstractQueuedSynchronizer(AQS).acquire()方法(图中E处),tryAcquire()尝试加锁,如果加锁失败会通过addWaiter()方法把当前线程加入队列,然后通过acquireQueued()阻塞
- tryAcquire()(图中F处)由子类FairSync、NonfairSync重写(图中G1、G2处,G2调用H2),加锁核心逻辑在G1、H2
非公平锁加锁流程
核心代码(H2处):
- 首先判断当前状态,若 c==0 说明没有线程占用该锁,并在占用锁成功之后将锁指向当前线程;
- 如果 c != 0 说明有线程正拥有了该锁,而且若占用该锁就是当前线程(锁重入),则将 state 加 1。
这段的代码只是简单地++acquires,并修改status值,是因为当前并没有锁竞争,获取锁的本身就是当前线程,所以直接通过setStatus修改state就可以了,不用CAS。
公平锁加锁流程
核心代码(G1处):
公平锁加锁流程与非公平锁唯一的区别就是在设置state前多了一步操作:判断当前线程是不是队列中被唤醒的线程,如果是就执行CAS,否则获取资源失败
解锁操作
解锁流程
源码
ReentrantLock.unlock()方法直接调用其内部类sync.release()方法,这个方法是继承自其父类AbstractQueuedSynchronizer
// java.util.concurrent.locks.ReentrantLock
public void unlock() {
sync.release(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer,子类Sync直接继承
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// java.util.concurrent.locks.ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 减少重入次数
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 当前线程不是持有锁的线程,抛出异常
boolean free = false;
// 如果持有线程全部释放,将当前独占锁持有线程设置为null,并更新state
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
AbstractQueuedSynchronizer(AQS)
AQS特点
ReentrantLock底层基于AbstractQueuedSynchronizer(AQS)实现,AQS抽象类定义了一套多线程访问共享资源的同步模板,是一个依赖状态(state)的同步器。
解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,简单的说,AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。对应的设计模式就是模版模式
一般通过定义内部类Sync继承AQS,将同步器所有调用都映射到Sync对应的方法;
AQS基于链表实现的双向同步队列,在CLH的基础上进行了变种,CLH是单向队列,其主要特点是自旋检查前驱节点的locked状态。
而AQS同步队列是双向队列,每个节点也有状态waitStatus,而其并不是一直对前驱节点的状态自旋判断,而是自旋一段时间后阻塞让出cpu时间片(上下文切换),等待前驱节点主动唤醒后继节点。
AQS同步队列的head节点是一个空节点,没有记录线程node.thread=null,其后继节点才是实质性的有线程的节点,当最后一个有线程的节点出队列后,不需要想着清空队列,同时下次有新节点入队列也不需要重新实例化队列。
所以当队列为空head=tail=null,第一个线程节点入队列时,需要先初始化
队列节点部分属性:
static final class Node {
// 共享锁标识
static final Node SHARED = new Node();
// 独占锁标识
static final Node EXCLUSIVE = null;
// 线程等待状态,初始为0
volatile int waitStatus;
// 表示当前结点已取消调度。当超时或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
static final int CANCELLED = 1;
// 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为 SIGNAL。
static final int SIGNAL = -1;
// 表示结点等待在 Condition 上,当其他线程调用了 Condition 的 signal() 方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
static final int CONDITION = -2;
// 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
static final int PROPAGATE = -3;
// 前驱结点
volatile Node prev;
// 后置结点
volatile Node next;
// 持有的线程对象
volatile Thread thread;
Node nextWaiter;
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
}
加锁
当用户加锁时会执行 AbstractQueuedSynchronizer.acquire(int arg)方法,代码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) && //尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Node.EXCLUSIVE(null) 为独占锁模式, Node.SHARED 为共享锁,所以ReentrantLock为独占锁
selfInterrupt();
}
- tryAcquire(arg)方法去尝试获取锁,它调用的是其子类的重写方法(G1、G2处)
- 若尝试获取锁失败,则调用 AQS.addWaiter(Node.EXLUSIVE) 方法将当前线程放入到队列;
- AQS.acquireQueued()方法把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞;
正常入队流程和队列初始化示意:
addWaiter方法负责把当前无法获得锁的线程包装为一个Node节点添加到队尾:
private Node addWaiter(Node mode) {
// 1.将当前线程构建成Node类型
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) { // 2.tail不为null,说明队列中已经有在排队的线程,进行入队操作
// 3.入队操作
node.prev = pred; // 3.1将新节点前节点设为tail节点
if (compareAndSetTail(pred, node)) { // 3.2将新节点通过CAS设置为tail节点,若设置失败跳到步骤7进入enq(),自旋直至成功入队
pred.next = node; // 3.3将原tail节点的next指向新节点(也就是新的tail节点)
return node; // 3.4返回入队成功的节点,传入acquireQueued()进行阻塞
}
}
enq(node); // 4.tail为null,说明队列还未初始化,直接进入enq()进行队列初始化和入队
return node;
}
private Node enq(final Node node) {
for (;;) { //自旋操作
Node t = tail;
if (t == null) { // 5.再次检查tail,为null说明队列还未初始化,如果不为null说明在并发情况下已被其他线程抢先初始化,直接进入步骤7
if (compareAndSetHead(new Node())) // 6.初始化队列,设置队列head和tail,如果成功然后进入下一轮循环将新节点入队列(步骤7),失败进入下一轮循环回到步骤5重新尝试初始化
tail = head; // head和tail都指向同一个空node,随着队列元素越来越多,tail跟着往后移动,head一直指向这个空节点,不代表任何线程
} else {
// 7.入队操作,与步骤3相同
// 进入这一步的除了本线程上一轮步骤6初始化成功了或是在并发情况下已被其他线程抢先初始化的情况,还有步骤3.2时失败会到这里
node.prev = t;
if (compareAndSetTail(t, node)) { // 失败继续自旋回到步骤5
t.next = node;
return t;
}
}
}
}
即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)。总而言之,addWaiter的目的就是通过CAS把当前现在追加到队尾,并返回包装后的Node实例。
/**
* acquireQueued的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 获取当前节点的前驱节点
if (p == head && tryAcquire(arg)) { // 2、如果p是头结点,说明当前节点在真实数据队列的首部,就尝试是否能获得锁,如果重试成功能则无需阻塞
setHead(node); // 获取锁成功,将当前node设置为头节点
p.next = null; // 便于GC垃圾回收
failed = false;
return interrupted; // return false
}
// 1、说明p不为头结点或者没有拿到锁(可能是非公平锁模式下被其他线程抢占了锁)
// 这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果上述都完成或者有异常抛出的时候,会判断是否是失败了,如果是,那么当前节点就去取消获取锁。
if (failed)
cancelAcquire(node);
}
}
这个方法中有一个自旋逻辑,如果2处(p == head && tryAcquire(arg))条件不满足,1处的parkAndCheckInterrupt会把当前线程阻塞。
当前线程移动到队首并被其他线程唤醒时,这个线程会继续自旋,执行到2处,会再次去尝试获取锁,直至成功。
/**
* setHead方法是把当前节点置为虚节点,但并没有修改waitStatus
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
/**
* 靠前驱节点判断当前线程是否应该被阻塞,如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 1、前置节点的waitStatus是SIGNAL(-1)状态的,那就直接park住了
return true;
if (ws > 0) {
// 2、前置节点的waitStatus > 0,表示当前节点是CANCEL状态,那么从前往后的遍历剔除状态是CANCEL的节点,返回false。
// acquireQueued方法的无限循环将递归调用该方法,直至1处返回true,导致线程阻塞
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 3、如果waitStatus等于0或者小于-1,那么就直接把前置节点的waitStatus改为SIGNAL(-1)状态,返回false。进入acquireQueued自旋,直至1处返回true,导致线程阻塞
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
解锁
public final boolean release(int arg) {
if (tryRelease(arg)) { // tryRelease如果返回true,说明该锁没有被任何线程持有
Node h = head;
if (h != null && h.waitStatus != 0) // 头结点不为空并且头结点的waitStatus不为0(非初始化节点情况),解除线程挂起状态
unparkSuccessor(h);
return true;
}
return false;
}
h == null 说明队列还未初始化
h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。
h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; // 获取头结点waitStatus
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next; // 获取当前节点的下一个节点
// 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
if (s == null || s.waitStatus > 0) {
s = null;
// 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark
if (s != null)
LockSupport.unpark(s.thread);
}