上一篇:
获取到锁的线程,在执行自己的代码, 没有获取锁的线程在使用 lock.lock() 获取锁
的时候阻塞了。
此时按照正常流程,获取锁的线程释放锁,看看会发生什么。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.unlock();
public void unlock() {
sync.release(1);
}
//AQS::release
public final boolean release(int arg) {
// tryRelease 把 状态 state 减去 arg 。
// 因为可重入,所以未必能够减成 0,
// 返回 true 表示,减成0了,锁释放
// 返回 false 表示,只是线程重入多次,释放一次而已
if (tryRelease(arg)) {
//进入:锁要释放了。
Node h = head;
if (h != null && h.waitStatus != 0)
/*
* unparkSuccessor 这个方法以前看过啊,
* 让方法的参数的节点的下一个节点解除阻塞。
* 如果下一个节点为空,或者下一个节点被取消,那就从 tail
* 开始向前找,找到离参数后面的最近的一个有效的节点,解除他的阻塞。
*/
unparkSuccessor(h);
return true;
}
return false;
}
//Sync::tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
/* 当前线程不是持有锁的线程,释放锁,抛异常, 所以:
* ReentrantLock lock = new ReentrantLock();
* lock.unlock(); //抛异常
* 一个线程调用 lock.unlock(); 前必须获取了锁
*/
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//因为可能是被线程重入了多次, unlock 一次,减一次
if (c == 0) {
//终于把 state 减成了 0, 把锁的持有者清空
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
线程中调用 lock.unlock(), 最后执行了 unparkSuccessor(h);
解除了 head 后面的一个节点的阻塞状态。 所以现在有线程已经被唤醒。
到现在就有两个线程:一个是:刚刚释放锁的线程,他一直都没有阻塞, 另一个是刚唤醒的
前面的示例是一个线程抢一次(获取在重入一次)然后释放就退出了, 如果放到循环中,
就会出现自己刚释放自己又去抢的情况。
for(;;) {
lock.lock()
...
lock.unlock()
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//AQS::acquireQueued
//这个方法又来了,前面说过,多个执行 lock.lock() 的线程阻塞在 ----A 上。
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)) { //--------------B
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //--------------A
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--------------A:
一个线程被唤醒, 而且是正常唤醒, 所以 parkAndCheckInterrupt() 返回 false 表示
不是被线程打断唤醒的。所以 ----A 处的 if 进不去。然后就来了一次循环。
--------------B
被唤醒的就是 head 的后一个节点的线程,所以此时 p 就是 head
而 tryAcquire(arg) 就真的能够返回 true 表示加锁成功吗? 未必,因为现在还是有两个
线程在争抢:(刚释放锁的线程 和 刚唤醒的线程即本线程)
只不过刚释放锁的线程再抢锁是重新调用 lock.lock
而唤醒线程枪锁的是在 ------B: tryAcquire(arg) 中进行的。
如果是释放锁的线程又抢到锁, 被唤醒线程在 ----B: tryAcquire(arg) 返回false
再次来到 -----A 中,又被阻塞
如果是唤醒线程抢到锁, 那么原来释放锁的线程通过调用 lock.lock, 一路执行,也来到
本方法,创建了自己的节点,把自己的节点添加到双向链表上, 然后也阻塞在 ----A
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
此时完成 获取锁,阻塞线程,释放锁,再获取,再阻塞 .... 的流程。
认知也发生了一些改变,前面提到,持有非公平锁的线程释放锁后, 那些因为没有获取锁
而阻塞的线程会一哄而上争抢锁。 看完源码后知道了,并不是所有被阻塞的线程都去争抢
锁了。释放锁后,只会唤醒一个线程去抢锁。他的竞争对手是 刚刚释放锁的线程,当然
还可能是全新的线程也第一次想通过 lock.lock 竞争锁。
唤醒的线程是 head 的后一个,或者head后,离head最近的,有效的一个节点。虽然是
非公平锁, 但还是有一点有序。 对于同在阻塞链表中的节点来说,是公平的, 阻塞添加
时都是添加到后面,唤醒都是先唤醒前面的,也体现了先来后到。
================================================================================
先看一个的现象:
private static void t2()
{
new Thread(()->{
Thread.currentThread().interrupt(); //中断自己先
System.out.println("中断1:" + Thread.currentThread().isInterrupted());
System.out.println("中断2:" + Thread.currentThread().isInterrupted());
MyUtils.sleep(1000);
System.out.println("中断3:" + Thread.currentThread().isInterrupted());
MyUtils.sleep(1000);
System.out.println("中断4:" + Thread.currentThread().isInterrupted());
System.out.println("执行完成");
}).start();
}
输出:
中断1:true
中断2:true
java.lang.InterruptedException: sleep interrupted ....
中断3:false
中断4:false
执行完成
线程开始并没有阻塞,但是上来就给自己一个中断。
中断1 为 true, 好理解, 但是 中断2 怎么还是 true ,不是说调用两次就能清除中断状态吗?
看一下源码:
内部会调用下面 native 方法,传递了 false。 传递 false 表示并不会擦除中断的状态
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
所有 中断2 还是 true 。
接着调用 sleep(1000) 。 因为当前线程时中断状态,所有,sleep 立刻返回,而且
抛出异常。 接下来中断状态变成 false, 感觉是被 sleep 中和了。
后面 在 sleep 就能正常sleep 了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private static void t2()
{
new Thread(()->{
Thread.currentThread().interrupt(); //中断自己先
System.out.println("中断1:" + Thread.interrupted());
System.out.println("中断2:" + Thread.currentThread().isInterrupted());
MyUtils.sleep(1000);
System.out.println("中断3:" + Thread.currentThread().isInterrupted());
MyUtils.sleep(1000);
System.out.println("中断4:" + Thread.currentThread().isInterrupted());
System.out.println("执行完成");
}).start();
}
输出:
中断1:true
中断2:false
中断3:false
中断4:false
执行完成
这里的 sleep 都是正常的,没有异常出现, 唯一的不同点在 中断1 后面的方法
//成员方法
public boolean isInterrupted() {
return isInterrupted(false);
}
//静态的
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
他们都调用了 native 方法。只是传递的参数不同。
传递 true 表明清除中断标记
传递 false 表明,不会清除中断标记。
private native boolean isInterrupted(boolean ClearInterrupted);
上面 isInterrupted 和 interrupted 两个方法时获取状态,以及是否擦除中断状态
而下面这个方法时行动。 中断线程,并设置为中断状态。 如果当前没有阻塞,先调用
这个方法,给线程设置了中断的状态,需要小心了,那些阻塞方法(sleep, wait,
countdownLatch.await阻塞等)遇到中断状态,会抛出异常立刻返回,
并把状态设置为 false
//成员方法:中断线程,把中断设置成 true
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
有了这些说明以后,再来看看,如果 lock.lock() 阻塞被 线程中断会怎么样。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private void t1() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Runnable r = () -> {
lock.lock();
try {
System.out.println("已获取锁:" + Thread.currentThread().getName());
sleep(10000);
} finally {
System.out.println("释放锁:" + Thread.currentThread().getName());
lock.unlock();
}
};
Runnable rBlock = () -> {
sleep(1000);
lock.lock();
try {
System.out.println("已获取锁:" + Thread.currentThread().getName());
System.out.println(Thread.currentThread().isInterrupted());
//可以使用 Thread.interrupted() 清除中断状态,否则 sleep 会抛出异常。
//System.out.println(Thread.interrupted());
sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
finally {
System.out.println("释放锁:" + Thread.currentThread().getName());
lock.unlock();
}
};
Thread t1 = new Thread(rBlock, "[线程1]");
Thread t2 = new Thread(r, "[线程2]");
t1.start();
t2.start();
new Thread(()->{
sleep(5000);
t1.interrupt();
System.out.println("已经打断线程1");
}, "[线程3]").start();
}
线程1先 sleep 1秒, 确保 线程2 能够得到锁。 线程2 获取锁并持有10秒, 线程1阻塞
在 lock() 上。5秒后,线程3中断线程1。 线程1 是否能够从 lock() 中解除阻塞并向下
运行呢? 是不可以的, 所有 lock.lock() 是不可被中断的 锁。lock.lock() 只能是
通过竞争,获取锁,才能解除阻塞。 10000 秒后 线程2 释放了锁。此时线程1 获取了锁
可是执行到 sleep 时, 立刻抛出 interrupted 异常。 接着释放锁,结束了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
为什么会这样,继续看源码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); //---------------------C
}
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; //---------------------B
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //---------------------A
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
线程2,获取了锁, 导致线程1 阻塞在 ----A 上, 此时线程1 被中断, 从 ----A 中返回。
并且是因为 中断返回的, 所以 ---A 处的 if 判断成立, interrupted 变量被赋值为 true.
接着下一次循环, 此时 线程2还在持有锁, 所以 线程1 又阻塞在 ---A 上。因此,
线程中断并不能让阻塞解除。 直到 线程2 释放了锁, 此时 线程1 再次被唤醒,从 ---A
中返回,然后获取锁成功, 代码从 ---B 处退出, 并返回 interrupted(此时为 true)
代码执行回到了 ---C: selfInterrupt() { Thread.currentThread().interrupt();}
这里又调用了一次,让线程中断,把线程的中断状态设置成 true.
所以运行到用户代码时,遇到 sleep 这样的阻塞方法,不会阻塞,而抛出异常。
所以写这段代码的时候一定要注意,自己的线程是否被中断过。 如果被中断过, 那么调用
一些阻塞的代码的是否可能会抛出异常。超出我们的预期。 所以如果想要擦除异常,使用
Thread.interrupted() 可以擦除线程的中断状态。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
到现在 lock.lock() 获取锁的方式就说完了,总结一下:
lock.lock() 是不可被线程中断的异常,没有获取到锁的线程在 一个双向链表中排队。
并被 unsafe.park 阻塞线程。 当锁释放, 总是链表的第一个 和 释放锁的线程 还有
新的线程 抢占锁。链表第一个线程如果没有抢到,继续阻塞, 如果其他两个没有抢到,
就会被加入到链表的尾部。 特别注意的是,如果线程曾经被打断,需要小心,下一次
的阻塞代码可能抛出异常。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果是公平锁,使用 lock.lock() 又是怎么样的公平呢
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
公平锁:
final void lock() {
acquire(1);
}
非公平锁:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
二者都调用的方法: acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
最后一处的差别:tryAcquire(arg) 被复写了, 实现不同:
公平锁:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//和 非公平锁的区别就是 多判断异常 hasQueuedPredecessors()
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;
}
非公平锁:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
和公平锁 tryAcquire 的实现就是少了 【!hasQueuedPredecessors() &&】 这个判断
}
hasQueuedPredecessors() 自己不是双向链表 head 的后面一个【感觉这个方法的命名
和文档描述并没有体现出方法的意图,甚至文档 中返回值的描述都是误导性的,
可能是文档描述的节点是不包含head的? 所以head后的节点是没有前节点的。
但是对于一个还未加入的线程来说,自己也应该是没有前节点的才对,反正现在很乱】
那些没有获取锁的线程都按照顺序被放到双向链表中。 此时持有锁的线程释放了锁。
按照非公平的锁,现在又三股势力在抢锁。1、阻塞链表中 head后节点(此时被唤醒),
2、刚刚释放锁的线程, 3、新的线程,第一次来抢。 非公平锁时,他们都有可能抢到。
如果时公平锁呢?抢锁的还是他们三个(这和抢不抢没有关系,因为他们线程没有阻塞,
正常运行而已)。 但是只能是 双向链表的 head 后置节点能抢到。原因就是这个方法:
hasQueuedPredecessors()。 释放锁的线程 和 新线程,他们现在都还不在双向链表中。
所以他们调用这个方法返回了 true, 结果外面取反,变成 false。 他们只能加入到链表。
所以公平和非公平的区别就是:释放锁以后,是不是剥夺了 释放锁的线程 和 新线程的
抢锁资格。共同点就是 双向链表中的众多节点,只能由 head 后面的那个节点去参与抢锁。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~