2020-6-5 22:49:21 唐涛

中断响应

对于关键字synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。
而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。

lockInterruptibly

public void lockInterruptibly() throws InterruptedException

  1. 如果当前线程未被中断,则获取锁。
  2. 如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
  3. 如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
  4. 如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以
    前,该线程将一直处于休眠状态: 1)锁由当前线程获得;或者 2)其他某个线程中断当前线程。
  5. 如果当前线程获得该锁,则将锁保持计数设置为 1。如果当前线程: 1)在进入此方法时已经设置了该线程的中断状态;或者 2)在等待获取锁的同时被中断。 则抛出 InterruptedException,并且清除当前线程的已中断状态。
  6. 在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或
    重入获取。

指定者: 接口 Lock 中的 lockInterruptibly
抛出: InterruptedException 如果当前线程已中断。

示例

  1. package tao;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. /**
  4. * @Author TangTao tangtao2099@outlook.com
  5. * @GitHub https://github.com/tangtaoshadow
  6. * @Website https://www.promiselee.cn/tao
  7. * @ZhiHu https://www.zhihu.com/people/tang-tao-24-36
  8. * @ClassName IntLock
  9. * @Note
  10. * @CreateTime 2020-06-05 22:20:00
  11. * @UpdateTime 2020-06-05 22:20:00
  12. */
  13. public class IntLock implements Runnable {
  14. public static ReentrantLock lock1 = new ReentrantLock();
  15. public static ReentrantLock lock2 = new ReentrantLock();
  16. int lock;
  17. /**
  18. * 控制加锁顺序,方便构造死锁
  19. *
  20. * @param lock
  21. */
  22. public IntLock(int lock) {
  23. this.lock = lock;
  24. }
  25. @Override
  26. public void run() {
  27. try {
  28. if (lock == 1) {
  29. System.out.println(Thread.currentThread().getName() + " 等待获取lock1 lock1.lockInterruptibly()");
  30. lock1.lockInterruptibly();
  31. System.out.println(Thread.currentThread().getName() + "获取lock1成功 lock1.lockInterruptibly() ==============");
  32. try {
  33. Thread.sleep(500);
  34. } catch (InterruptedException e) {
  35. }
  36. System.out.println(Thread.currentThread().getName() + "等待获取lock2 lock2.lockInterruptibly()");
  37. lock2.lockInterruptibly();
  38. System.out.println(Thread.currentThread().getName() + "获取lock2 lock2.lockInterruptibly() ================");
  39. } else {
  40. System.out.println(Thread.currentThread().getName() + "等待获取lock2 else lock2.lockInterruptibly()");
  41. lock2.lockInterruptibly();
  42. System.out.println(Thread.currentThread().getName() + "获取lock2 else lock2.lockInterruptibly() ---------------");
  43. try {
  44. Thread.sleep(500);
  45. } catch (InterruptedException e) {
  46. }
  47. System.out.println(Thread.currentThread().getName() + " 等待获取lock1 else lock1.lockInterruptibly()");
  48. lock1.lockInterruptibly();
  49. System.out.println(Thread.currentThread().getName() + " 成功获取lock1 else lock1.lockInterruptibly() ------------------------");
  50. }
  51. } catch (InterruptedException e) {
  52. e.printStackTrace();
  53. } finally {
  54. if (lock1.isHeldByCurrentThread()) {
  55. lock1.unlock();
  56. }
  57. if (lock2.isHeldByCurrentThread()) {
  58. lock2.unlock();
  59. }
  60. System.out.println(Thread.currentThread().getName() + ":线程退出");
  61. }
  62. }
  63. public static void main(String[] args) throws InterruptedException {
  64. IntLock r1 = new IntLock(1);
  65. IntLock r2 = new IntLock(2);
  66. Thread t1 = new Thread(r1);
  67. Thread t2 = new Thread(r2);
  68. t1.start();
  69. t2.start();
  70. Thread.sleep(5000);
  71. //中断其中一个线程
  72. t2.interrupt();
  73. }
  74. }

输出

  1. 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
  2. Picked up JAVA_TOOL_OPTIONS: -Duser.language=en
  3. Thread-1等待获取lock2 else lock2.lockInterruptibly()
  4. Thread-0 等待获取lock1 lock1.lockInterruptibly()
  5. Thread-1获取lock2 else lock2.lockInterruptibly() ---------------
  6. Thread-0获取lock1成功 lock1.lockInterruptibly() ==============
  7. Thread-0等待获取lock2 lock2.lockInterruptibly()
  8. Thread-1 等待获取lock1 else lock1.lockInterruptibly()
  9. java.lang.InterruptedException
  10. at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:958)
  11. at java.base/java.util.concurrent.locks.ReentrantLock$Sync.lockInterruptibly(ReentrantLock.java:161)
  12. at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:372)
  13. at tao.IntLock.run(IntLock.java:61)
  14. at java.base/java.lang.Thread.run(Thread.java:832)
  15. Thread-1:线程退出
  16. Thread-0获取lock2 lock2.lockInterruptibly() ================
  17. Thread-0:线程退出
  18. 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

示例

  1. package tao;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. /**
  5. * @Author TangTao tangtao2099@outlook.com
  6. * @GitHub https://github.com/tangtaoshadow
  7. * @Website https://www.promiselee.cn/tao
  8. * @ZhiHu https://www.zhihu.com/people/tang-tao-24-36
  9. * @ClassName TimeLock
  10. * @Note
  11. * @CreateTime 2020-06-05 22:36:00
  12. * @UpdateTime 2020-06-05 22:36:00
  13. */
  14. public class TimeLock implements Runnable {
  15. public static ReentrantLock lock = new ReentrantLock();
  16. @Override
  17. public void run() {
  18. try {
  19. if (lock.tryLock(2, TimeUnit.SECONDS)) {
  20. System.out.println(Thread.currentThread().getName() + " 成功获取 lock");
  21. Thread.sleep(3000);
  22. } else {
  23. System.out.println(Thread.currentThread().getName() + " 获取 lock 超时 ======");
  24. }
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. } finally {
  28. System.out.println(Thread.currentThread().getName() + " 即将释放 lock");
  29. lock.unlock();
  30. }
  31. }
  32. public static void main(String[] args) {
  33. TimeLock tl = new TimeLock();
  34. Thread t1 = new Thread(tl);
  35. Thread t2 = new Thread(tl);
  36. t1.start();
  37. t2.start();
  38. }
  39. }

输出

  1. Thread-1 成功获取 lock
  2. Thread-0 获取 lock 超时 ======
  3. Thread-0 即将释放 lock
  4. Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
  5. at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:175)
  6. at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1006)
  7. at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:494)
  8. at tao.TimeLock.run(TimeLock.java:34)
  9. at java.base/java.lang.Thread.run(Thread.java:832)
  10. Thread-1 即将释放 lock

公平锁

如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。通过 ReentrantLock() 来设定。

实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现**成本比较高,性能却非常低下,因此,在默认情况下,锁是非公平的。

ReentrantLock的几个重要方法整理如下。

  • lock():获得锁,如果锁已经被占用,则等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,则返回true,失败返回false。该方法不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
  • unlock():释放锁。

重入锁的实现中,主要包含三个要素。

  1. 原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有了。
  2. 等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
  3. 阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。