2020-6-5 22:49:21 唐涛
中断响应
对于关键字synchronized
来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。
而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。
lockInterruptibly
public void lockInterruptibly() throws InterruptedException
- 如果当前线程未被中断,则获取锁。
- 如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
- 如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
- 如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以
前,该线程将一直处于休眠状态: 1)锁由当前线程获得;或者 2)其他某个线程中断当前线程。 - 如果当前线程获得该锁,则将锁保持计数设置为 1。如果当前线程: 1)在进入此方法时已经设置了该线程的中断状态;或者 2)在等待获取锁的同时被中断。 则抛出 InterruptedException,并且清除当前线程的已中断状态。
- 在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或
重入获取。
指定者: 接口 Lock 中的 lockInterruptibly
抛出: InterruptedException
如果当前线程已中断。
示例
package tao;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author TangTao tangtao2099@outlook.com
* @GitHub https://github.com/tangtaoshadow
* @Website https://www.promiselee.cn/tao
* @ZhiHu https://www.zhihu.com/people/tang-tao-24-36
* @ClassName IntLock
* @Note
* @CreateTime 2020-06-05 22:20:00
* @UpdateTime 2020-06-05 22:20:00
*/
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加锁顺序,方便构造死锁
*
* @param lock
*/
public IntLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
System.out.println(Thread.currentThread().getName() + " 等待获取lock1 lock1.lockInterruptibly()");
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "获取lock1成功 lock1.lockInterruptibly() ==============");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "等待获取lock2 lock2.lockInterruptibly()");
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "获取lock2 lock2.lockInterruptibly() ================");
} else {
System.out.println(Thread.currentThread().getName() + "等待获取lock2 else lock2.lockInterruptibly()");
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "获取lock2 else lock2.lockInterruptibly() ---------------");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " 等待获取lock1 else lock1.lockInterruptibly()");
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 成功获取lock1 else lock1.lockInterruptibly() ------------------------");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + ":线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(5000);
//中断其中一个线程
t2.interrupt();
}
}
输出
C:\tao-depend\open-jdk\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51345:C:\Program Files\JetBrains\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath C:\tao-program\test\testjava\out\production\testjava tao.IntLock
Picked up JAVA_TOOL_OPTIONS: -Duser.language=en
Thread-1等待获取lock2 else lock2.lockInterruptibly()
Thread-0 等待获取lock1 lock1.lockInterruptibly()
Thread-1获取lock2 else lock2.lockInterruptibly() ---------------
Thread-0获取lock1成功 lock1.lockInterruptibly() ==============
Thread-0等待获取lock2 lock2.lockInterruptibly()
Thread-1 等待获取lock1 else lock1.lockInterruptibly()
java.lang.InterruptedException
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:958)
at java.base/java.util.concurrent.locks.ReentrantLock$Sync.lockInterruptibly(ReentrantLock.java:161)
at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:372)
at tao.IntLock.run(IntLock.java:61)
at java.base/java.lang.Thread.run(Thread.java:832)
Thread-1:线程退出
Thread-0获取lock2 lock2.lockInterruptibly() ================
Thread-0:线程退出
Process finished with exit code 0
线程t1和t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再请求lock1。因此,很容易形成t1和t2之间的相互等待。在这里,对锁的请求,统一使用
lockInterruptibly()
方法。 这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。在代码第49行,主线程main处于休眠状态,此时,这两个线程处于死锁的状态。 在代码第51行,由于t2线程被中断,故t2会放弃对lock1的申请,同时释放已获得的lock2。这个操作导致t1线程可以顺利得到lock2而继续执行下去。
锁申请等待限时
除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。
给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的。我们可以使用tryLock()
方法进行一次限时的等待。ReentrantLock.tryLock()
方法也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true
。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false
。这种模式不会引起线程等待,因此也不会产生死锁。
public boolean tryLock()
仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
- 如果该锁没有被另一个线程保持,并且立即返回
true
值,则将锁的保持计数设置为 1。
即使已将此锁设置为使用公平排序策略,但是调用tryLock()
仍将立即获取锁(如果有可用的),
而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公
平性也如此。如果希望遵守此锁的公平设置,则使用tryLock(0, TimeUnit.SECONDS)
,它几乎是等效的(也检测中断)。 - 如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回
true
。 - 如果锁被另一个线程保持,则此方法将立即返回
false
值。
指定者:
接口 Lock 中的 tryLock
返回:
如果锁是自由的并且被当前线程获取,或者当前线程已经保持该锁,则返回 true
;否则返回 false
示例
package tao;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author TangTao tangtao2099@outlook.com
* @GitHub https://github.com/tangtaoshadow
* @Website https://www.promiselee.cn/tao
* @ZhiHu https://www.zhihu.com/people/tang-tao-24-36
* @ClassName TimeLock
* @Note
* @CreateTime 2020-06-05 22:36:00
* @UpdateTime 2020-06-05 22:36:00
*/
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 成功获取 lock");
Thread.sleep(3000);
} else {
System.out.println(Thread.currentThread().getName() + " 获取 lock 超时 ======");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 即将释放 lock");
lock.unlock();
}
}
public static void main(String[] args) {
TimeLock tl = new TimeLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
}
}
输出
Thread-1 成功获取 lock
Thread-0 获取 lock 超时 ======
Thread-0 即将释放 lock
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:175)
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1006)
at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:494)
at tao.TimeLock.run(TimeLock.java:34)
at java.base/java.lang.Thread.run(Thread.java:832)
Thread-1 即将释放 lock
公平锁
如果我们使用synchronized
关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。通过 ReentrantLock()
来设定。
实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现**成本比较高,性能却非常低下,因此,在默认情况下,锁是非公平的。
ReentrantLock
的几个重要方法整理如下。
lock()
:获得锁,如果锁已经被占用,则等待。lockInterruptibly()
:获得锁,但优先响应中断。tryLock()
:尝试获得锁,如果成功,则返回true
,失败返回false
。该方法不等待,立即返回。tryLock(long time, TimeUnit unit)
:在给定时间内尝试获得锁。unlock()
:释放锁。
重入锁的实现中,主要包含三个要素。
- 原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有了。
- 等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
- 阻塞原语
park()
和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。