多线程 Part 3 - ReentrantLock
1. ReentrantLock 加锁原理
简单测试一下两个线程分别给count变量做加1操作20000次
class Process implements Runnable
{
int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment()
{
count++;
}
public void process()
{
for (int i = 0; i < 20000; i++)
{
lock.lock();
try
{
increment();
}
finally
{
lock.unlock();
}
}
}
@Override
public void run() {
process();
}
}
public class TestLocks {
public static void main(String[] args) throws InterruptedException {
Process obj = new Process();
Thread t1 = new Thread(obj);
Thread t2 = new Thread(obj);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(obj.count);
}
}
输出:
40000
但当我们使用ReentrantLock加锁的时候,到底发生了什么?
通过源码分析(基于Java 8):
- 从执行
lock()
方法开始
lock.lock();
- 找到内置 sync 的
lock()
方法
public void lock() {
sync.lock();
}
- 找到公平锁的实现方法
final void lock() {
acquire(1);
}
- 进入
acquire()
方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 进入
tryAcquire()
方法,如果锁自由,那判断当前线程要不要排队
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread(); //得到当前线程
int c = getState(); //得到锁状态
if (c == 0) {
//如果锁状态为0,即锁自由,判断当前线程要不要排队
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果锁状态不为0,判断当前线程是否持有锁,是的话,进行重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; //acquires值为1,给锁状态再加1
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); //更新锁状态
return true;
}
return false;
}
- 我们进入
hasQueuedPredecessors()
方法,判断当前线程要不要去队列排队,要的话返回true
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
- 如果当前线程能拿到锁的话,回到第3步
acquire()
方法就执行结束了,否则要继续下去,我们先进入addWaiter()
方法
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
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) {
//队尾节点next指向当前节点,当前节点prev指向队尾节点,更新队尾为当前节点
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果队列不存在,进入enq()
enq(node);
return node;
}
- 进入
enq()
方法,把当前node放入队列
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail; //拿到队尾
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) //初始化队首,此时注意,head的thread为空
tail = head; //队首队尾相连
} else {
//有队列了,把当前节点放入队列
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 当前node进入队列了,然后我们进入
acquireQueued()
方法。如果当前node是队列里第一个排队的,自旋一次;如果不是要去设置前一个节点的状态,然后回来再自旋一次,再没拿到锁才阻塞
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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)) { //如果前一个节点是head那说明当前节点是队列里第一个排队的,尝试获取锁,自旋一次
setHead(node); //如果得到锁了,队列往前移,当前节点变成队首
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果没有拿到锁,准备park阻塞
if (shouldParkAfterFailedAcquire(p, node) && //如果前一个节点状态不是-1,要置为-1,然后循环回去再尝试一遍tryAcquire(arg),也就是再一次自旋
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 进入
shouldParkAfterFailedAcquire()
方法,要把前一个节点状态置为Node.SIGNAL也就是-1
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //拿到前一个节点的状态
if (ws == Node.SIGNAL) //判断前一个节点的状态是不是Node.SIGNAL
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
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); //把前一个节点的状态置为-1
}
return false;
}
- 而
parkAndCheckInterrupt()
方法才真正的阻塞线程
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
ReentrantLock加锁的流程图
有几个需要注意的地方:
- 队列里面的队首head的thread永远都是null
- 在head之后的一个node才是第一个在排队的线程
- node进队列后,阻塞之前,会有两次自旋尝试能否拿到锁
- 每个node的状态永远都是后一个node来设置,这样设计是因为线程自己睡眠之后便无法执行代码,只能由后面线程来设置它的节点为睡眠状态
- 持有锁的线程永远都不在队列里
这里我们再看几个细节:
如果在队列里的线程自旋拿到锁了,会怎么样?
队列会往前移,但这个过程有哪些步骤呢?在 acquireQueued()
方法里,源码是这样的:
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
其中 setHead()
是这样的:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
那就很清晰了,分为5步:
- 把当前线程节点设为head头结点
- thread置空
- 当前节点的前一个节点置空
- 原本的head头结点的后一个节点置空,也就是把原本的头结点从队列里分离开来了,那它不指向任何引用了,就会被垃圾回收
- failed 置为 false
hasQueuedPredecessors()
方法在判断要不要排队的时候,到底在判断什么?
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这个方法返回false代表不用排队,而且注意这个方法只有在锁状态为0时才会执行
它也分为几个步骤:
-
h != t ?
h != t 就说明队列里有线程正在排队,反过来 h==t 说明队列里没有线程在排队,这个时候无非是队列没有初始化,就是初始化了,队首队尾相连,而还没有线程进来排队。既然没有线程在排队(包括当前线程自己),当前线程就不需要去排队,此时锁状态为0,当前线程可以直接拿到锁。 -
(s = h.next) == null ?
如果会执行这个,说明 h!=t,也就是队列里有线程在排队。那这里这句就是在判断头结点的后一个是不是为空。一般情况下,队首在队列里好好的,队列里又有线程在排队,队首后一个不会为空,返回false。但如果为空的话,那后一句 s.thread 就根本无法执行了。 -
s.thread != Thread.currentThread() ?
队列里有线程在排队,队首后一个也确实不为空,那就要判断当前线程是不是第一个在排队的线程。如果是的话,就不用排队,锁状态为0可以去拿锁;不是的话,说明前面还有线程在排队,那当前线程当然只能乖乖排队。
2. ReentrantLock 解锁原理
还是从源码分析:
- 解锁从
unlock()
开始
public void unlock() {
sync.release(1);
}
- 进入
release()
方法,如果释放锁成功,返回true
public final boolean release(int arg) {
if (tryRelease(arg)) { //尝试释放锁
//如果释放锁成功,找到队列的队首,唤醒队列里第一个排队的线程,如果有的话
Node h = head;
if (h != null && h.waitStatus != 0) //这里判断有没有队列,然后队列里有没有线程在排队
unparkSuccessor(h);
return true;
}
return false;
}
- 进入
tryRelease()
,如果释放锁成功返回true
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //release值为1,将锁状态减1
if (Thread.currentThread() != getExclusiveOwnerThread()) //如果当前线程,也就是要释放锁的线程,不是持有锁的线程,那就出问题了
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //如果锁状态为0,那锁就释放成功了,将持有锁的线程置空
free = true;
setExclusiveOwnerThread(null);
}
setState(c); //更新锁状态
return free;
}
- 如果释放锁成功,会进入
unparkSuccessor()
方法,唤醒传入节点后一个线程,如果有的话
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
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) //队首节点在睡眠时状态是-1,这里把它置为0
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) {
//如果队首后的节点是空,或者状态大于1,也就是被取消,我们从队列尾部往前遍历,寻找第一个在队列里面非空切状态小于等于0的线程节点
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); //找到要被唤醒的线程节点,然后唤醒这个线程
}
ReentrantLock解锁的流程图
需要注意的地方:
- 每次 tryRelease() 都是给锁状态减1,那么减1之后的锁状态一定为0吗?当然是不一定的,也就是说原有的锁状态大于1。这种情况就是重入。在加锁时,如果要加锁的是持有锁的线程,那就会重入,锁状态会继续加1。那显然,如果加锁了两次,也要解锁两次,锁状态才会回到0,否则队列里排队的线程永远拿不到锁。
- 那么锁状态会被减为负吗?如果加锁一次,试图解锁两次,第一次解锁成功之后,线程就不再拥有锁,第二次再解锁就会报异常,因为当前线程不持有锁却试图解锁,非法。
这里我们就写个代码测试一下锁重入
t1线程先启动,故意加锁两次,再解锁两次
public class TestLocks {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock(true); //公平锁
Thread t1 = new Thread(()->{
//加锁两次
reentrantLock.lock();
reentrantLock.lock();
try {
System.out.println("t1");
}
finally {
//解锁两次
reentrantLock.unlock();
reentrantLock.unlock();
}
});
Thread t2 = new Thread(()->{
reentrantLock.lock();
try {
System.out.println("t2");
}
finally {
reentrantLock.unlock();
}
});
t1.start();
Thread.sleep(100); //让t1先启动,t2再启动
t2.start();
t1.join();
t2.join();
System.out.println("end"); //等待线程都执行完毕后,打印代表程序执行结束
}
}
输出:
t1
t2
end
t1 加锁两次,解锁两次后,t2拿到了锁,执行完毕程序结束
现在我们让 t1 加锁两次,但只解锁一次
public class TestLocks {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock(true); //公平锁
Thread t1 = new Thread(()->{
//加锁两次
reentrantLock.lock();
reentrantLock.lock();
try {
System.out.println("t1");
}
finally {
//解锁一次
reentrantLock.unlock();
}
});
Thread t2 = new Thread(()->{
reentrantLock.lock();
try {
System.out.println("t2");
}
finally {
reentrantLock.unlock();
}
});
t1.start();
Thread.sleep(100); //让t1先启动,t2再启动
t2.start();
t1.join();
t2.join();
System.out.println("end"); //等待线程都执行完毕后,打印代表程序执行结束
}
}
输出:
t1
t2 没有拿到锁。t1 释放一次锁后,在队列里的 t2 被唤醒了一次,但由于锁状态不为0,它没有获取锁,继续睡眠了。
3. ReentrantLock 处理中断
举个例子,银行ATM,银行卡插进去了,要你输密码,你过了3分钟都没有输密码,会怎么样?ATM就会把你的银行卡吐出来,中断业务受理。这就相当于队列里的线程一直在等待唤醒,等待锁状态为0,而持有锁的线程一直在办业务,等待时间超出了限制,那队列里的线程就应该中断,不等了。
ReentrantLock 有个方法 lockInterruptibly()
就可以实现这个工作,我们写个程序测试一下
public class TestLocks {
static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void process()
{
try {
reentrantLock.lockInterruptibly(); //用lockInterruptibly()加锁
System.out.println(Thread.currentThread().getName()+" start"); //打印当前线程名字
Thread.sleep(5000); //处理业务很久
System.out.println(Thread.currentThread().getName()+" finish"); //业务结束
} catch (InterruptedException e) {
//lockInterruptibly()抛出的异常,当有线程中断时就会抛出异常
System.out.println("interrupt " +Thread.currentThread().getName()+" thread");
e.printStackTrace();
}
finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{ process(); },"t1");
Thread t2 = new Thread(()->{ process(); },"t2");
Thread t3 = new Thread(()->{ process(); },"t3");
//t1, t2, t3的顺序启动线程
t1.start();
Thread.sleep(100);
t2.start();
Thread.sleep(100);
t3.start();
//过1秒,t2 中断
Thread.sleep(1000);
t2.interrupt();
}
}
输出:
- t1 开始,先得到锁,t2,t3入队列
- 过了1秒,t2不等了,中断
- 再过一会儿 t1 完成,释放锁
- t3 得到锁,开始执行,执行完,释放锁
我们去源码看看lockInterruptibly()
到底做了什么
- 进入
lockInterruptibly()
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
- 进入
acquireInterruptibly(1)
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg)) //同样的尝试获取锁的方法
doAcquireInterruptibly(arg);
}
- 进入
doAcquireInterruptibly()
,这个其实和之前加锁过程的acquireQueued()
很像
/**
* Acquires in exclusive interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE); //把node放入队列,这个和之前lock()入队的过程一样
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor(); //这里自旋一次判断是不是可以拿到锁,队列要不要往前移,和lock()时的也一样
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) && //这里和之前也一样,如果park了返回ture
parkAndCheckInterrupt()) //这个我们需要重新进去看看
throw new InterruptedException(); //这里不一样,抛出了异常
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 我们进到
parkAndCheckInterrupt()
看看
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
似乎没有什么高深的地方,阻塞了线程之后,返回了当前线程是不是被中断的判断,但其实这里大有文章。
在这里需要先了解一下,中断线程t2.interrupt()
做了啥?
其实它啥也没做,就是设置了一个标记,真正的中断工作是本地方法实现的而不是Java实现的。
//interrupt()方法源码
public void interrupt() {
if (this != Thread.currentThread()) {
checkAccess();
// thread may be blocked in an I/O operation
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupted = true;
interrupt0(); // inform VM of interrupt
b.interrupt(this);
return;
}
}
}
interrupted = true; //设置标记
// inform VM of interrupt
interrupt0(); //这个是本地方法
}
那Thread.interrupted()
做了啥,写个测试看一看
public class TestInterrupt {
public static void main(String[] args) {
Thread.currentThread().interrupt();
System.out.println(Thread.interrupted());
System.out.println(Thread.interrupted());
}
}
输出:
true
false
首先当前线程先中断,Thread.interrupted()
判断了一下,确实它中断了。但是当我再让它判断一次的时候,它却返回了false。这说明Thread.interrupted()
第一次判断的时候,还作了一次标记重置。这属于非用户行为。标记重置的目的是为了方便用户下次中断线程时,可以正常操作。
那我们按照这个思路再回到源码看,回到parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
假设 t2 现在已经park了,然后用户中断了 t2,那么这里parkAndCheckInterrupt()
就会返回true。回到doAcquireInterruptibly()
,此时就会抛出中断的异常
private void doAcquireInterruptibly(int arg){
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
/***省略这里的代码***/
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException(); //此时这里就会执行,抛出中断的异常,然后往finally走
}
} finally {
if (failed) //此时failed为true,进入cancelAcquire()
cancelAcquire(node);
}
}
- 进入
cancelAcquire()
,简单的说,就是把node的thread置空,把node的状态置为CANCELLED,并且从队列里移除,交给垃圾回收。如果node原本是队列里第一个排队的,那就唤醒它的后一个节点。这也就是为什么队列里可能存在状态为CANCELLED的节点;也解释了之前t1, t2, t3那个例子中,t2中断后,t3如何顺利拿到锁。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
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
}
}