Lock

作者: 我可能是个假开发 | 来源:发表于2023-02-18 16:42 被阅读0次

    Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。

    一、再造管程的理由

    解决死锁的其中一个方案-破坏不可抢占条件,synchronized 没有办法解决。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。

    我们希望的是:对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

    互斥锁解决的问题:

    • 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
    • 支持超时:如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
    • 非阻塞地获取锁:如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

    这三种方案可以全面弥补 synchronized 的问题。体现在 API 上,就是 Lock 接口的三个方法:

    • 支持中断的API:void lockInterruptibly() throws InterruptedException;
    • 支持超时的API:boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    • 支持非阻塞获取锁的API:boolean tryLock();

    二、保证可见性

    class X {
      private final Lock rtl = new ReentrantLock();
      int value;
      public void addOne() {
        // 获取锁
        rtl.lock();  
        try {
          value+=1;
        } finally {
          // 保证锁能释放
          rtl.unlock();
        }
      }
    }
    

    利用了 volatile 相关的 Happens-Before 规则。Java SDK 里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。根据相关的 Happens-Before 规则:

    • 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
    • volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
    • 传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。
    class SampleLock {
      volatile int state;
      // 加锁
      lock() {
        // 省略代码无数
        state = 1;
      }
      // 解锁
      unlock() {
        // 省略代码无数
        state = 0;
      }
    }
    

    三、可重入

    1.可重入锁

    所谓可重入锁,顾名思义,指的是线程可以重复获取同一把锁。

    class X {
      private final Lock rtl = new ReentrantLock();
      int value;
      public int get() {
        // 获取锁
        rtl.lock();         ②
        try {
          return value;
        } finally {
          // 保证锁能释放
          rtl.unlock();
        }
      }
      public void addOne() {
        // 获取锁
        rtl.lock();  
        try {
          value = 1 + get(); ①
        } finally {
          // 保证锁能释放
          rtl.unlock();
        }
      }
    }
    

    当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。

    2.可重入函数

    多个线程可以同时调用某个函数,每个线程都能得到正确结果;

    同时在一个线程内支持线程切换,无论被切换多少次,结果都是正确的。多线程可以同时执行,还支持线程切换,所以,可重入函数是线程安全的。

    相关文章

      网友评论

        本文标题:Lock

        本文链接:https://www.haomeiwen.com/subject/imdbkdtx.html