ReentrantLock锁简介
-
ReentrantLock是可重入的独占锁,同时也提供了可以响应中断的加锁方法
-
ReentrantLock通过Sync继承AQS来使用AQS提供的锁框架的独占模式,有公平锁非公平锁两种
abstract static class Sync extends AbstractQueuedSynchronizer
//非公平锁
static final class NonfairSync extends Sync
//公平锁
static final class FairSync extends Sync
- AQS使用state值来表示锁资源,对于独占锁,当有线程抢锁时会尝试设置state,设置成功即表示加锁成功
AQS字段
-
AQS中使用Node来包装未拿到锁的线程,抢锁失败时线程会把自己的Node结点放入阻塞队列
-
对于阻塞队列来说,头结点可以理解为拥有锁的线程结点,当持有锁的线程释放锁时,会唤醒头结点的后继的第一个正常结点
-
对于AQS的共享模式和条件队列,本节暂不涉及
//AQS中用来包装线程的结点
static final class Node {
//当前结点是共享模式
static final Node SHARED = new Node();
//当前结点是独占模式
static final Node EXCLUSIVE = null;
//当前结点处于取消状态
static final int CANCELLED = 1;
//当前结点需要唤醒它的后继节点
static final int SIGNAL = -1;
//当前结点在条件队列中的状态
static final int CONDITION = -2;
//当前结点为传播状态
static final int PROPAGATE = -3;
//当线程被包装入结点时,初始默认状态为0
//当前结点的状态,可选值为0, SIGNAl(-1), CANCELLED(1), CONDITION(-2), PROPAGATE(-3)
volatile int waitStatus;
//阻塞队列中前驱节点
volatile Node prev;
//阻塞队列中后继结点
volatile Node next;
//Node封装的线程
volatile Thread thread;
//阻塞队列中用来标识是独占锁还是共享锁
//条件队列中用来指向下一个结点
Node nextWaiter;
//当前结点是否是共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回当前结点的前驱节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//用来初始化头结点,或者SHARED结点
Node() { // Used to establish initial head or SHARED marker
}
//把结点加入阻塞队列时使用的构造方法
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//把结点加入条件队列时使用的构造方法
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
//头结点:当有线程释放锁时,会唤醒头结点后面的第一个正常结点
private transient volatile Node head;
//尾结点
private transient volatile Node tail;
//锁资源:初始状态为0,即没有线程持有锁,每加锁一次,state就加1(独占模式下)
private volatile int state;
//当前锁的值
protected final int getState() {
return state;
}
以公平锁为例解析AQS的加锁流程
-
公平锁中lock()提供了拿锁操作,具体加锁流程是调用AQS中的模板方法
-
公平锁重写了AQS中的tryAcquire()方法
//lock()方法直接调用了AQS的acquire()
final void lock() {
acquire(1);
}
//AQS的acquire()方法:拿锁
public final void acquire(int arg) {
//tryAcquire(arg):尝试获取锁,获取成功返回true,获取失败返回false,具体后面会写
//addWaiter(Node.EXCLUSIVE), arg):将当前线程封装成Node,并加入阻塞队列,具体后面会写
//acquireQueued(addWaiter(Node.EXCLUSIVE), arg):尝试获取锁,并挂起线程,线程被唤醒后,会再次尝试拿锁,具体后面会写
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//acquireQueued()方法返回的是挂起过程中是否被中断唤醒
//selfInterrupt()方法:线程给自己一个中断信号
selfInterrupt();
}
//这是AQS的方法,公平锁重写了此方法
protected final boolean tryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//AQS中的state值:表示当前锁资源
int c = getState();
//当前AQS处于无锁状态
if (c == 0) {
//公平锁,需要检查一下,阻塞队列中是否有结点等待
//hasQueuedPredecessors():true 阻塞队列中有结点在等待
//hasQueuedPredecessors():false 阻塞队列中没有结点等待
//compareAndSetState():CAS操作设置state值,拿锁
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
//设置当前线程为持有锁线程
setExclusiveOwnerThread(current);
return true;
}
}
//c != 0 此时需要检查一下,当前线程是不是持有锁的线程,ReentrantLock可以重入
//当前线程就为持有锁线程
else if (current == getExclusiveOwnerThread()) {
//重入之后的state值
int nextc = c + acquires;
//判断nextc是否已经超过int最大值,如果锁重入了很多次,c是Integer.MAX_VALUE时再增加就越界了
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//设置state值
setState(nextc);
return true;
}
//情况一:c == 0 时,阻塞队列已经有线程在等待了,公平锁不能抢锁
//情况二:c == 0 时,CAS竞争失败
//情况三:c > 0 时,持有锁的线程不是自己
return false;
}
- 当线程抢锁失败后,会把自己封装成Node结点,加入阻塞队列,如果是第一个抢锁失败的线程会为阻塞队列构造一个空的头结点
//AQS的addWaiter():将线程封装为Node结点尝试入队
private Node addWaiter(Node mode) {
//构建Node,把当前线程封装到Node中
//如果是独占锁,mode是Node.EXCLUSIVE
//如果是共享锁,mode是Node.SHARED
Node node = new Node(Thread.currentThread(), mode);
//队尾结点
Node pred = tail;
//队列中已经有结点
if (pred != null) {
//当前节点的前驱指向尾结点
node.prev = pred;
//CAS操作将队列尾节点设置为当前Node:入队成功
if (compareAndSetTail(pred, node)) {
//原队尾结点的next指向现队尾结点
pred.next = node;
return node;
}
}
//情况一:当前队列是空队列
//情况二:CAS竞争失败
//继续入队,具体后面会写
enq(node);
return node;
}
//AQS中的enq():将当前Node加入阻塞队列
private Node enq(final Node node) {
//自旋入队
for (;;) {
//尾结点
Node t = tail;
//当前队列是空队列,当前线程有可能是第一个获取锁失败的线程
if (t == null) {
//当前线程需要入队,但队列为空,当前线程需要创建一个头结点
//因为第一个持有锁的线程不会自己入队,需要后面拿锁失败的线程自己创建一个空的头结点new Node()
//设置自己创建的头结点
if (compareAndSetHead(new Node()))
tail = head;
//队列不为空,把自己加入队列
} else {
//入队:如果失败继续自旋
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
//返回旧的尾结点,即当前结点的前驱结点
return t;
}
}
}
}
-
线程的Node结点加入阻塞队列后会被阻塞挂起,在挂起之前会尝试拿锁,被唤醒之后继续尝试拿锁
-
由于当前加锁流程是不响应中断的,线程被中断唤醒后会把自己的中断标记置为false,不过acquireQueued()方法会返回线程是否被中断过
-
响应中断的加锁会在线程被中断唤醒后抛出异常,后面具体有写
//AQS的acquireQueued():尝试拿锁,失败就挂起,醒来后继续拿锁,失败继续挂起
//Node:就是包装当前线程的结点,此时已经加入阻塞队列
//arg:当前线程拿锁时用来设置state值时
final boolean acquireQueued(final Node node, int arg) {
//正常情况下拿锁后被置为false
//不正常情况会在finally中取消该结点
boolean failed = true;
try {
//当前线程是否被中断过
boolean interrupted = false;
//自旋..
for (;;) {
//情况一:第一次for循环时,线程尚未park
//情况二:线程被unpark唤醒
//当前节点的前置节点
final Node p = node.predecessor();
//p == head:当前节点为头结点下一个节点,此时有权利去抢锁
//tryAcquire(arg) true:头结点对应的线程已经释放锁,此时当前结点正好抢到了锁
// false:头结点还持有锁,当前节点仍旧要被park
if (p == head && tryAcquire(arg)) {
//拿到锁之后,设置自己为头节点。
setHead(node);
//将旧的头结点的next置为null
p.next = null; // help GC
//当前线程正常拿锁
failed = false;
//返回当前线程是否被中断过
return interrupted;
}
//shouldParkAfterFailedAcquire():判断当前线程是否需要被挂起,在这个方法中会保证前置结点状态为SIGNAL
//true:当前线程会被parkAndCheckInterrupt()挂起
//parkAndCheckInterrupt():挂起当前线程,线程被唤醒后返回当前线程的中断标记
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
//parkAndCheckInterrupt()== true:当前线程是被中断信号唤醒的
//interrupted置为true,线程被中断唤醒
interrupted = true;
}
} finally {
//什么时候failed为true?上面拿锁过程出现异常或者Error,方法不能正常返回了,此时取消当前结点
if (failed)
//这个方法后面会写,在响应中断加锁流程里会具体写
cancelAcquire(node);
}
}
//AQS的shouldParkAfterFailedAcquire():1.node结点的前置结点pred为取消状态,越过取消状态的结点,返回false
// 2.node结点的前置结点pred为默认状态,将前置结点的状态设置为SIGNAl,返回false
// 3.node结点的前置结点pred为SIGNAl状态,返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前置结点的状态: 默认状态 SIGNAL状态 CANCELED状态
int ws = pred.waitStatus;
//前置结点是SIGNAL状态:前置结点可以唤醒当前结点的节点,直接返回true,让parkAndCheckInterrupt()park当前线程
//这是因为在线程被park之前,需要保证当前结点的前置结点可以唤醒当前结点,即前置结点状态为SIGNAL
if (ws == Node.SIGNAL)
return true;
//前置结点是CANCELED状态:已经被取消
if (ws > 0) {
do {
//设置前置结点,一直向前找
node.prev = pred = pred.prev;
//向前寻找waitStatus <= 0 的结点
} while (pred.waitStatus > 0);
//找到后,将找到的结点后继设为当前结点
//也就是这两者之间的CANCELED状态的结点就出队了
pred.next = node;
} else {
//前置结点的状态是默认状态0
//将前置结点的状态设置为SIGNAl,表示前置结点点释放锁之后需要唤醒当前结点
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//AQS的parkAndCheckInterrupt():挂起当前线程,在其醒来后返回中断标志位,并重置中断标志为false
private final boolean parkAndCheckInterrupt() {
//利用LockSupport.park挂起当前线程
LockSupport.park(this);
//线程如何被唤醒? 1.被前驱结点用LockSupport.unpark唤醒,此时线程中断标记是false;
// 2.被中断唤醒,此时线程中断标记是true;
//Thread.interrupted()返回当前线程的中断标志位,然后把中断标志位置为false;
return Thread.interrupted();
}
解析ReentrantLock解锁流程
- ReentrantLock解锁流程是调用的AQS的模板方法,其中Sync重写了tryRelease()方法
//ReentrantLock提供的解锁入口,直接调用AQS的模板方法
public void unlock() {
sync.release(1);
}
//AQS的release()
public final boolean release(int arg) {
//尝试释放锁 true:当前线程已经完全释放锁
false:当前线程尚未完全释放锁,因为锁是可重入的,重入几次,释放几次
if (tryRelease(arg)) {
//什么情况下有头结点?
//情况一:当持锁线程拥有锁时,有其它线程想要拿锁时失败,此时队列是空队列,该线程会创建一个空结点
//情况二:线程成为头结点的下一个结点时,被唤醒拿锁会成为新的头结点
Node h = head;
//头结点不为空且状态不为0:什么时候不为0?
//当有结点插入时,会把前置结点状态置为-1,当然取消状态会被出队
if (h != null && h.waitStatus != 0)
//唤醒后继节点,具体后面会写
unparkSuccessor(h);
return true;
}
return false;
}
//ReentrantLock中的Sync重写的tryRelease()
protected final boolean tryRelease(int releases) {
//减去释放的值
int c = getState() - releases;
//说明当前线程并未持锁,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//是否完全释放锁
boolean free = false;
//完全释放锁
if (c == 0) {
free = true;
//设置持锁为空
setExclusiveOwnerThread(null);
}
//更新AQS的state值
setState(c);
return free;
}
//AQS的unparkSuccessor():唤醒当前结点的第一个正常的后继结点
private void unparkSuccessor(Node node) {
//当前节点的状态
int ws = node.waitStatus;
//-1 Signal
if (ws < 0)
//改成零的原因:当前结点要唤醒后继结点了,后继结点会把当前结点出队
compareAndSetWaitStatus(node, ws, 0);
//s是当前结点的第一个后继节点
Node s = node.next;
//s 什么时候为空?
//情况一:当前结点是尾结点
//情况二:当新结点入队未完成时,下面从后向前查找就是这个原因
//s.waitStatus > 0 当前结点的后继节点是取消状态,找一个最近的可以被唤醒的结点
if (s == null || s.waitStatus > 0) {
//为什么从后向前查找?
//因为如果此时有新结点正在入队,
//第一步是新节点的prev指向尾结点
//第二步设置新节点为尾结点 ----------- 如果这一步完成后,此时开始查找,最后一个链还没接上,因此需要从后向前找
//第三步是旧尾结点next指向新尾结点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//找到后,唤醒该结点,让它去抢锁
if (s != null)
LockSupport.unpark(s.thread);
}
公平锁与非公平锁区别
-
公平锁和非公平锁在抢锁入口处:公平锁直接调用AQS模板方法,而非公平锁先尝试抢锁
-
公平锁和非公平锁,都重写了tryAcquire()方法:公平锁在抢锁前会判断阻塞队列是否有结点在等待锁,而非公平锁直接尝试抢锁
-
非公平锁相对不公平的相同之处:抢锁失败的处理流程相同,解锁流程相同
ReentrantLock加锁响应中断
-
响应中断的加锁方法lockInterruptibly()和lock()不同之处
-
1.在拿锁前先判断中断标志位
-
2.在线程拿锁失败被挂起期间如果被中断唤醒,抛出中断异常,在抛出异常前取消该结点cancelAcquire(),下面具体写
//AQS的cancelAcquire():将该结点设置为取消状态
private void cancelAcquire(Node node) {
//空判断
if (node == null)
return;
//当前线程已经不参与排队拿锁了,所以把结点的线程置为空
node.thread = null;
//当前结点的前驱
Node pred = node.prev;
//向前找非取消状态的结点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//情况一:predNext是当前结点
//情况二:predNext是 ws > 0 的节点
Node predNext = pred.next;
//将当前结点状态设置为取消状态
node.waitStatus = Node.CANCELLED;
//情况一:当前结点是队尾结点,直接CAS改变队尾为pred:pred是前面找到的当前结点前面的非取消状态的结点
if (node == tail && compareAndSetTail(node, pred)) {
//修改pred.next=null
compareAndSetNext(pred, predNext, null);
} else {
//情况二:当前结点不是队尾,也不是头结点的后继结点
//保存结点状态
int ws;
//pred != head :说明当前结点不是head.next节点
//(ws = pred.waitStatus) == Node.SIGNAL:说明pred的状态是SIGNAL状态 pred状态可能是0,也可能取消排队变为CANCELLED
//(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)):假设pred状态是 <= 0 则设置pred状态为SIGNAL状态
//pred.thread != null:pred并未取消
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//pred -> node -> node.next
//让pred.next = node.next:当node.next结点被唤醒后,该结点的shouldParkAfterFailedAcquire()会跳过取消状态的结点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//情况三:当前结点是头结点的后继结点,此时队列情况是: head -> 当前结点 -> 第三个结点...
//这个时候用unparkSuccessor()去唤醒那个第三个结点,和情况二一样它会调用shouldParkAfterFailedAcquire()跳过取消状态的结点
//在shouldParkAfterFailedAcquire()后,队列情况是head-> 第三个node 当前结点被出队
unparkSuccessor(node);
}
node.next = node; // help GC
}
}