JUC--009--locks5
作者:互联网
前面已经说了 ReentrantLock 使用 lock.lock() 方式获取锁,而且测试过这中获取锁的方式
如果获取不到锁就会阻塞,而且无法通过线程中断的方式解除阻塞。如果使用线程中断
试图解除阻塞,虽然不会成功,但是会把线程的状态改成已中断,这个可能会影响后续代码。
同时也说了公平锁和非公平锁的区别。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
获取锁的四种方式:
lock.lock(); 阻塞式获取锁
lock.lockInterruptibly(); 阻塞式获取锁,可以被中断
lock.tryLock() 非阻塞式获取锁,返回 true 表示获取了锁
lock.tryLock(timeOut, unit) 带超时的阻塞式获取锁。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
现在看一下 lock.lockInterruptibly(); 如何做到可以被中断的。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
示例代码:
开启两个子线程:线程1 进来先sleep(1000), 确保 线程2 能够获取锁。线程2 获取锁以后
准备长时间持有。 所以 线程1 阻塞在 lock.lockInterruptibly() 上。 但是主线程 5秒后
让 线程1 中断。线程1 中断后,从 阻塞中抛出异常。进入 finally 中执行。 这时一定
要判断一下,当前线程是否持有锁。已经看过释放锁的代码,没有锁的线程试图释放锁会
抛出异常。
private void t1() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Runnable r = () ->
{
try {
lock.lock();
System.out.println("已获取锁:" + Thread.currentThread().getName());
sleep(1000000);
} finally {
lock.unlock();
}
};
Runnable rBlock = () ->
{
sleep(1000);
try {
lock.lockInterruptibly();
System.out.println("--已获取锁:" + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
finally {
if(lock.isHeldByCurrentThread()) {
lock.unlock();
} else {
System.out.println("--未曾拥有:" + Thread.currentThread().isInterrupted());
}
}
};
Thread t1 = new Thread(rBlock, "[线程1]");
Thread t2 = new Thread(r, "[线程2]");
t1.start();
t2.start();
sleep(5000);
t1.interrupt();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.lockInterruptibly();
//ReentrantLock::lockInterruptibly
//调用的方法是不同的:lock.lock() 调用的是 acquire(1);
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//AQS::acquireInterruptibly
/*
* 如果线程已经存在中断状态,下面进入第一个 if 直接抛出异常。
* lock.lock() -----1
* lock.lockInterruptibly() -----2
* 假设我们想通过 1 获取锁,然后使用 2 重入一次。结果在 1 中阻塞。期间被中断
* 只是 lock.lock 不能中断,所以没有什么表现,结果等到 1 获取了锁,然后
* 使用 2 重入一次的时候,却有表现了,直接抛出异常。这个可能会出乎意料。
*
* 前面已经多次提到过这个,这里最后一次重复了, 想要不抛出异常,清除线程中断状态。
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//此时再尝试一次是不是能够获取锁
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
//AQS::doAcquireInterruptibly
//这个代码看着好眼熟啊, CountDownLatch::await 的调用代码和这个很像,
//即这个方法: doAcquireSharedInterruptibly, 只他是共享的,这里的这个是独占的
/*
* 这个方法就不细说了,创建一个属于当前线程的节点,添加到双向链表,
* 把自己前面的节点的 状态设置成 SIGNAL, 然后自己通过 unsafe.park 阻塞
*
* 情况1: 自己线程并没有被中断,而是有人释放了锁,如果本线程Node就是 head 后面
* 的那一个,结果被唤醒,去争抢锁,如果失败了在这里循环一次,继续阻塞。
* 如果成功抢到锁,从 --OUT 处出去, finally 的代码条件不满足,不执行
* 情况2:还在阻塞的时,线程被中断,从 --wake 处返回 true, 进入 if 语句,抛出异常
* 执行 finally 中的代码(条件满足了), finally 中的代码就是 把自己的节点
* 删除,如果自己还有后续节点,也唤醒自己的下一个节点。 然后返回到用户代码
* 这里抛出的异常没有被捕获,直接抛给用户代码。
*
*/
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return; //----------OUT
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //----------wake
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
关于 lock.lockInterruptibly() 的代码执行流程就结束了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
第三种获取锁的方式: lock.tryLock()
tryLock() 不会阻塞,而是立刻返回。根据返回值判断是否获取了锁。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
使用示例:
如果没有获取锁,那就去干点别的事情,然后循环的使用 tryLock() 尝试获取锁。
获取锁以后执行任务,最后还有释放锁
private void t1() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Runnable r = () ->
{
try {
lock.lock();
System.out.println("已获取锁:" + Thread.currentThread().getName());
sleep(5000);
} finally {
lock.unlock();
}
};
Runnable rBlock = () ->
{
sleep(100); //确保线程2获取锁
while(true) {
if(lock.tryLock()) {
System.out.println("已获取锁:" + Thread.currentThread().getName());
try {
System.out.println("执行任务");
sleep(1000);
System.out.println("执行任务--完成,准备退出");
break;
} finally {
System.out.println("释放锁");
lock.unlock();
}
} else {
System.out.println("没有获取锁,干点别的");
sleep(1000);
}
}
System.out.println("线程退出");
};
Thread t1 = new Thread(rBlock, "[线程1]");
Thread t2 = new Thread(r, "[线程2]");
t1.start();
t2.start();
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tryLock() 方法的 java doc 上介绍,这个方法时无视公平原则的。即使时公平锁,也有
很多的线程再排队等待获取锁,但是这个方法,如果锁可用,他就会去抢。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.tryLock()
public boolean tryLock() {
return sync.nonfairTryAcquire(1); //难怪他会无视公平原则。
}
//Sync::nonfairTryAcquire
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;
}
没了,代码就这么多,使用这种方式获取锁, 要么获取到,要么获取不到,但自己不会
像前面的那些获取锁的方式,如果获取不到就创建一个Node节点, 添加到双向链表,并委
托自己的前置节点将自己唤醒。tryLock() 来去自由,感觉还洒脱。所以他也不存在中断,
排队等麻烦事。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.tryLock(timeOut, unit): 带超时的阻塞式获取锁方式。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~、、
使用代码:
tryLock(timeOut, unit) 带超时的获取锁。
获取锁立刻返回, 否则阻塞指定的时长,自动被唤醒,再次尝试获取锁。
阻塞时,如果自己被唤醒,也会参与抢锁行动,阻塞时也能被中断,所以会抛出中断异常。
使用代码意图:
先确保 线程2 获取锁,并持有锁10后释放。 线程1 开始带超时的阻塞 3 秒,但是 2 秒后,
主线程让 线程1 中断, 所以线程1抛出异常,然后异常被捕获,继续循环,再次带超时的
阻塞 3 秒。 此时线程2 仍然没有释放锁,超时时间到后,自动解除阻塞,返回 false,去
做了其他事情,然后再次循环, 再次尝试获取锁,又带超时的开始阻塞 ....
线程2 持有锁10秒后 释放了锁, 线程1立刻解除阻塞,开始执行代码。
执行完成后 break 退出,并释放锁。
private void t1() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Runnable r = () ->
{
try {
lock.lock();
System.out.println("已获取锁:" + Thread.currentThread().getName());
sleep(10000);
} finally {
lock.unlock();
}
};
Runnable rBlock = () ->
{
sleep(100); //确保线程2获取锁
while(true)
{
try {
System.out.println("准备获取锁:" + Thread.currentThread().isInterrupted());
if(lock.tryLock(3, TimeUnit.SECONDS))
{
System.out.println("已获取锁:" + Thread.currentThread().getName());
try {
System.out.println("执行任务");
sleep(1000);
System.out.println("执行任务--完成,准备退出");
break;
} finally {
System.out.println("释放锁");
lock.unlock();
}
} else {
System.out.println("没有获取锁,干点别的");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程退出");
};
Thread t1 = new Thread(rBlock, "[线程1]");
Thread t2 = new Thread(r, "[线程2]");
t1.start();
t2.start();
sleep(2000);
t1.interrupt();
}
输出:
已获取锁:[线程2]
准备获取锁:false
java.lang.InterruptedException .....
准备获取锁:false
没有获取锁,干点别的
准备获取锁:false
没有获取锁,干点别的
准备获取锁:false
已获取锁:[线程1]
执行任务
执行任务--完成,准备退出
释放锁
线程退出
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tryLock(long timeout, TimeUnit unit)
java doc 上介绍,如果是公平锁,那么他会遵守公平规则。 但是可以这样
if(lock.tryLock() || lock.tryLock(timeout, unit)) { ... } 先无视规则的
获取一次,只有获取不到锁,然后再来遵守规则。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lock.tryLock(3, TimeUnit.SECONDS)
//Sync::tryLock
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
//AQS::tryAcquireNanos
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//如果线程已经又中断状态,直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
/*
* tryAcquire(arg) 的实现,公平锁和非公平锁是有区别的。
* 所以 tryLock(timeout, unit) 非遵守公平锁的原则。
*
* tryLock(timeout, unit) 再次尝试获取锁,如果获取不到,就继续。
*/
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
//AQS::doAcquireNanos
//这个代码看着也熟悉,CountDownLatch.await(xx, xx) 调用的方法和
//这个很像,那个是共享模式, 这里的是独占模式。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
/*
* 如果 tryLock(小于等于0的数), 如果执行到这里之前能够获取锁,就获取了。
* 如果不能获取锁,来到这里,自己不会被加到队列,不会被阻塞,而是立刻返回。
*
* 这样的话,我们就能实现一个遵守公平规则的 tryLock(), 即
* tryLock(0, unit) 。 按照公平规则获取锁,获取不到也不阻塞
*/
if (nanosTimeout <= 0L)
return false;
//未来到期的相对时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//自己不是被打断方式唤醒(即正常唤醒),或者自己还没有被阻塞过。
//如果自己是 head 的后一个, 参与抢锁行动
if (p == head && tryAcquire(arg)) {
//抢到锁, 把head干掉,自己变成新 head
setHead(node);
p.next = null; // help GC
failed = false;
//获取锁,正常退出, finally 中的不用执行了。
//因为 head 被干掉,自己变成 head, 所以相当于把自己删除了,
//而且自己获取了锁,也没有必要唤醒下一个起来尝试抢一下锁。
return true;
}
//离到期(超时)剩余的时间
nanosTimeout = deadline - System.nanoTime();
/*
* 这里的 if 内部是一个出口:
* 情况1:第一次进来,把自己的 node 刚添加进去,结果时间到到期了。
* 此时自己没有获取锁,而且 finally 种的条件满足,会把自己
* 刚刚添加进来的 node 删除掉
* 情况2:像情况1一样,自己是第一次进来,虽然第一次循环这里不满足,
* 但是自己到期时间已经小于 spinForTimeoutThreshold(1000纳秒)
* 此时如果让线程阻塞不太划算, 1000纳秒太短,刚阻塞就要解开。
* 因此就不阻塞了, 在这个循环中转2圈,时间就耗掉了。所以
* 情况2就是没有阻塞,在这转圈耗时间,待满足退出,执行 finally
* 情况3:到期时间还有点长,阻塞了,然后超时后自动解除阻塞,再来一次
* 循环,这里就满足了,返回 false, 执行 finally
* 不管哪种情况,如果代码从这个出口出去的,就表示并没有获取锁,
* 而且执行 finally, 把自己的 node 从 双向链表中删除,如果有需要还要
* 唤醒自己的下一个节点。
*/
if (nanosTimeout <= 0L)
return false;
//把自己的前置节点的状态设置成 SIGNAL
if (shouldParkAfterFailedAcquire(p, node) &&
//剩余时间还很长,大于这个常数 1000 纳秒
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout); //待超时阻塞
//超时前,自己被中断,从阻塞中返回,抛出异常
//执行 finally , 清除自己的 node, 如果有需要,还要唤醒下一个节点
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
//从双向链表中清除参数 node , 如果有需要同时唤醒 node 的下一个节点
cancelAcquire(node);
}
}
根据上面的源码: 如果自己的线程被打断,那么抛出的异常没有被捕获,会抛给用户代码。
而且 在抛出异常前使用 Thread.interrupted() 获取线程是否被打断,这个方法会
清除线程的中断状态的, 所以用户代码会收到一个异常,但是获取中断状态是 false,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
到这里四种获取锁的方式说完了0
方式 | 阻塞 | 可否打断 | 遵守公平规则
------------------------+--------------+--------------+-----------------
lock.lock() | 永久阻塞 | 不可 | 遵守
------------------------+--------------+--------------+-----------------
lock.lockInterruptibly()| 永久阻塞 | 可 | 遵守
------------------------+--------------+--------------+-----------------
lock.tryLock() | 非阻塞 | --- | 不遵守
------------------------+--------------+--------------+-----------------
lock.tryLock(x, unit) | 超时阻塞 | 可 | 遵守
标签:JUC,Thread,--,lock,阻塞,获取,线程,tryLock,009 来源: https://blog.csdn.net/szw727/article/details/114371795