现在的位置: 首页 > 云计算 > 正文

ReentrantLock 是如何来控制锁的占有状态

2020年02月19日 云计算 ⁄ 共 1718字 ⁄ 字号 评论关闭

  你已经知道管程 synchronized 是通过锁对象所关联的 Monitor 对象的计数来标识线程对锁的占有状态,那么你知道 ReentrantLock 是如何来控制锁的占有状态吗?

  Lock 锁采用了与 Monitor 对象计数完全不同的一种方式,它依赖了并发包中的 AbstractQueuedSynchronizer (队列同步器简称 AQS ) 来实现在多线程情况下对锁状态的控制。那么 AQS 是如何保证在多线程场景中锁状态可以正常控制呢。

  主要使用了如下 3 个技术点:

  1. volatile 关键字首先 AQS 中定义了一个 volatile 修饰的 int 变量 state,大家都知道 volatile 是实现无锁编程的关键技术。它有内存可见性和禁止指令重排序的功效,当多个线程去争夺锁的占有权的时候,如果有其中任何一个线程已经持有锁,它会设置 state 加 1。这样由于 volatile 的可见性功效,其他线程会争取锁权限的时候发现锁已经被其他线程持有,就会进入等待队列。当持有锁的线程使用结束后,释放锁,会将 state 减 1,然后去唤醒队列中等待的线程。

  2.CAS(compare and swap) 算法你是否存在疑问,当线程将 state 加 1 的时候是如何保证只有一个线程在做这个操作呢,毕竟在加 1 操作的时候线程尚未获得锁,所以加 1 操作存在竞争关系。虽然 volatile 可以解决线程间的内存可见性问题,但是仍然存在多个线程竞争 state 更新的问题,这就要使用我们的 CAS 算法来解决这个问题了,compare and swap 中文意思是比较并且替换。它提供的功能是让我们提供一个预期值和一个即将要设置为的值,如果此时内存中的值正好是我们预期的值,则直接将值设置为我们即将要设置为的值。拿竞争 state 举例,所有线程都希望 state 的值为 0 (0 代表没有线程占用),然后自己将 state 设置为 1(占有锁)。在 AQS 中 由如下方法来帮我完成这件事情:

  protected final boolean compareAndSetState(int expect, int update) {

  // See below for intrinsics setup to support this

  return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}

  你注意到了,该方法并不会直接帮我们完成这件事情而是一个 Unsafe 的对象完成了实际的操作,Unsafe 类提供了大量的原子方法,但是这些方法都是 native 标识的,代表它们是 JVM 提供的本地 C++ 方法,底层本质是对 CPU CAS 指令的一个封装。这样 volatile 的内存可见性特效结合 CAS 算法就完美的解决了多线程并发获取锁的安全操作。当然啦,Unsafe 正如其名,寓意为不安全,不推荐开发者直接使用,只是在 Java SDK 中使用到了。

  3.双向队列到此你是否还存在疑问,多个线程竞争锁,一个线程获取到了锁,那其他线程怎么办呢?其他线程其实同样进入了阻塞等待状态(此处也使用了 Unsafe 的 park 本地方法),与 synchronized 不同是,AQS 内部提供了一个双向队列(不然怎么叫队列同步器呢,哈哈),队列的节点定义如下:

  static final class Node {

  /**省略部分属性**/

  volatile Node prev;

  volatile Node next;

  volatile Thread thread;

  Node nextWaiter;

  /**省略部分方法**/

  }

  每一个进入等待队列的线程都会被关联到一个 Node,所有等待的线程按照先后顺序会组成一个双向队列(组成队列的过程中同样使用到了 CAS 算法 和 volatile 特性,不然多线程并发的给队列结尾添加节点也会存在竞争问题),等待持有锁的线程释放锁后唤醒队列头部的节点中关联的线程(此处唤醒同样使用了 Unsafe 的 unpark 本地方法),这样就有序的控制了线程对共享数据的并发访问。

  如上我们介绍了 Lock 锁中用到的三个重要技术点和基本实现原理,相信问题回答到这一步面试官已经默默心里想:小伙子有点东西啊。

抱歉!评论已关闭.