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;}@Overridepublic 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.IntLockPicked up JAVA_TOOL_OPTIONS: -Duser.language=enThread-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.InterruptedExceptionat 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();@Overridepublic 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 成功获取 lockThread-0 获取 lock 超时 ======Thread-0 即将释放 lockException in thread "Thread-0" java.lang.IllegalMonitorStateExceptionat 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(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。
