ReentrantLock
1、概念和基本原理
1、可重入性
可重入是指同一个线程可以多次获取同一把锁。例如,在一个方法中获取了ReentrantLock
锁,然后在这个方法内部调用了另一个也需要获取该锁的方法,此时线程可以成功获取锁,而不会被阻塞。这就好比一个人有一把自己房间的钥匙,他可以多次进入自己的房间,每次进入相当于线程对锁的一次获取。
2、独占锁性质
ReentrantLock
是独占锁,在同一时刻,只有一个线程可以获取该锁。当一个线程获取了锁之后,其他线程如果也想获取这把锁,就会被阻塞,直到锁被释放。这就像是一个只有一个入口的房间,一次只能有一个人进入。
3、实现原理
它通过一个内部的同步器(AbstractQueuedSynchronizer
,简称 AQS)来实现锁的获取和释放机制。AQS 维护了一个等待队列,当线程获取锁失败时,会被封装成一个节点加入到等待队列中等待锁的释放。当锁被释放时,AQS 会按照一定的规则(公平或非公平)唤醒等待队列中的线程来尝试获取锁。
2、使用方法
1、基本操作
class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public int increment() {
lock.lock();
try {
count++;
return count;
} finally {
lock.unlock();
}
}
}
首先需要创建
ReentrantLock
对象,例如:ReentrantLock lock = new ReentrantLock();
。然后通过lock.lock()
方法来获取锁。如果锁当前没有被其他线程持有,那么获取锁的线程可以立即获得锁并继续执行后续代码。如果锁已经被其他线程持有,那么当前线程会被阻塞,直到获取到锁为止。当线程完成了对共享资源的操作后,需要通过lock.unlock()
方法来释放锁。如果不释放锁,其他等待该锁的线程将永远无法获取锁,这可能会导致程序出现死锁的情况。
公平锁与非公平锁
公平锁
ReentrantLock
可以通过构造函数参数来指定是否为公平锁。公平锁的特点是,等待时间最长的线程会优先获取锁。例如:ReentrantLock fairLock = new ReentrantLock(true);
创建了一个公平锁。在公平锁模式下,线程获取锁的顺序遵循先来后到的原则,等待队列中的线程按照 FIFO(先进先出)的顺序获取锁。
非公平锁
如果不指定构造函数参数或者传入false
,则创建的是一个非公平锁,这也是ReentrantLock
的默认模式。非公平锁的优势在于它的性能通常比公平锁要好。如果刚好锁处于可用状态,那么当前线程就可以直接获取锁,而不必等待队列中的线程先获取,不会引起线程的上下文切换。例如:ReentrantLock unfairLock = new ReentrantLock(false);
或者ReentrantLock unfairLock = new ReentrantLock();
创建的是非公平锁。
3、源码解释
构造方法
/**
* 默认构造方法
默认使用非公平锁。从上面的公平锁和非公平锁的对比克制
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 指定是否使用公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
3.1 ReentrantLock加锁原理
/**
* 非公平锁的实现
*/
static final class NonfairSync extends Sync {
/**
* 首先尝试加锁,加锁失败才会进入阻塞队列
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
/**
* 公平锁的实现方式
*/
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
从上面两个类的源码可知,公平锁和非公平锁都会调用acquire()方法
/**
* tryAcquire()尝试加锁
* acquireQueued()进入队列
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//重新设置线程中断标识,因为acquireQueued里面调用了 Thread.interrupted()方法清除了标志位
selfInterrupt();
}
下面我们看下 tryAcquire
和 acquireQueued
这两个方法。
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//当前无线程加锁
if (c == 0) {
//判断队列中 prev 是否有执行者
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;
}
}
//非公平锁的实现方式
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;
}
由此可见公平锁和非公平锁的的区别。公平锁的在加锁之前判断队列中是否有元素存在,如果不存在,才会尝试加锁。而非公平锁,则是直接进行加锁。
下面我们看下线程是如何加入阻塞队列中
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// 将元素插入队列,必要的时候进行链表的初始化
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//初始化tail 和 head 指针。这里对 head 指针进行加锁,因为此时 head 指针不能被释放掉。如果 head 被释放锁之后,这个链表的后续都不能连接起来。
if (compareAndSetHead(new Node()))
tail = head;
} else {
//首先将线程节点链接到 prev 节点上。
node.prev = t;
//正常情况下,线程节点 cas 成功会将 tail 指针指向线程节点
if (compareAndSetTail(t, node)) {
//如果这个时候 tail 节点已经释放,将会 cas 失败,那么t.next = node就无法赋值
t.next = node;
return t;
}
}
}
}
能够进入到该方法说明 线程节点已经进入到队列中,先尝试一下判断该节点是否已经释放,
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//因为锁唤醒的时候,不一定能拿到锁。所以这里进行了死循环
for (;;) {
final Node p = node.predecessor();
//如果该节点的前一个节点不是 head,就没有必要进行 tryAcquire().因为轮不到该节点来释放
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//判断前面的节点是否取消,用于判断当前线程节点能否安全的去阻塞。如果前一节点已经是取消状态,自己的睡眠就不安全。因为无法被唤醒操作。
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞方法,并且判断是否是中断
parkAndCheckInterrupt())
//是中断唤醒
interrupted = true;
}
} finally {
// tryAcquire是用户自定义的方法,如果这块内容出现了异常,则需要在这里进行兜底操作
if (failed)
cancelAcquire(node);
}
}
方法参数: Node pred 前一节点,Node node 是当前节点
Node.SIGNAL = -1 : 正常等待状态。如果Node.waitStatus = SIGNAL。则后面的线程一定会被唤醒,有责任去唤醒其他的后续节点
Node.CANCELED = 1 :取消状态,也就是无效状态
Node.CONDITION= -2 : 表示在某个条件下 线程阻塞
Node.PROPAGATE= -3 :表示传播。如果当前线程节点是Node.PROPAGATE,则进行下一个节点的唤醒操作
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 如果前一个节点的状态是正常等待状态,线程节点的状态设置成等待即可
*/
return true;
if (ws > 0) {
/*
* 如果前一个节点状态是取消状态,则进行遍历前面的节点,知道找到正常等待的节点
*/
do {
// 这里跳过的节点,为什么不需要手动回收?
// 因为跳过的节点,没有 GC Root,垃圾回收器会自动回收
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()
方法
private final boolean parkAndCheckInterrupt() {
//unsafe操作。线程阻塞
LockSupport.park(this);
//判断线程是否中断了,因为阻塞队列中的线程节点,无法响应中断
return Thread.interrupted();
}
Thread.interrupted(); 方法作用: 获取线程的标志位,判断是否被中断。检查的时候,会清除标志位,设置成 0
Thread.interrup() 方法作用: 给线程信息设置 标志位
cancelAcquire(node)
方法
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
//将线程信息回收。因为这个地方是线程节点需要取消操作
node.thread = null;
// 前面有取消的节点,这里帮助链表把取消的节点取消掉。
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
// 如果是 tail 指针,这里的直接释放本节点
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 如果本节点之后有个新的入队的节点。
int ws;
//如果不是头节点
if (pred != head &&
//如果前面的节点是可唤醒的
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
//除了判断状态,还需要判断 thread 的信息是否为空。因为上面的代码可以到,node.thread 是先设置为 null。后面设置 node.waitStatus 的状态。这个时候有可能被其他的线程操作,导致信息变更
pred.thread != null) {
//设置下一个节点
Node next = node.next;
//这里帮助链接,没有进行死循环进行。
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//如果是头节点,直接唤醒
unparkSuccessor(node);
}
//将本身自己的节点指向本节点,用于自动取消操作
node.next = node; // help GC
}
}
3.2 ReentrantLock释放锁
/**
*
*/
public final boolean release(int arg) {
//释放本节点
if (tryRelease(arg)) {
Node h = head;
//唤醒后续节点,伪唤醒机制,因为这里只是尝试去唤醒后续节点,不一定能够成功。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor() 方法
// 这里的 Node 是 head 节点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
//当前节点的 head,正在唤醒其他的节点,head 节点就不需要进行唤醒了。
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//从前往后唤醒
Node s = node.next;
if (s == null || s.waitStatus > 0) {
//如果后续节点是空,则尝试从后往前唤醒。在入队的时候,当 tail 指针 cas 成功之后,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);
}
线程的操作方法
unpark()
unpark()
方法用于为指定线程提供一个许可。如果目标线程当前正被park()
方法阻塞,那么这个许可会让该线程立即解除阻塞状态并继续执行。如果目标线程还没有调用park()
方法,那么这个许可会被保存起来,等到线程调用park()
方法时,它会发现许可已经可用,就不会被阻塞park()
park()
是一种用于阻塞线程的机制。当一个线程调用park()
方法时,它会检查一个许可(permit)是否可用。如果许可不可用,线程将被阻塞,进入等待状态,直到获得许可或者被中断。- 许可的初始状态是不可用的。线程在调用
park()
方法时,就好像在等待一个信号来继续执行。这个信号就是许可。如果没有其他线程调用unpark()
方法(稍后介绍)来提供许可,线程就会一直处于阻塞状态总结:
两者谁先谁后调用都无所谓两者谁先谁后调用都无所谓
喜欢的可以关注我的公众号 程序员茶馆