ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

1. CAS

CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现

2. AQS

AQS:是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。
image.png
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

3. Lock()和unLock()实现原理

ReentrantLock提供了两个构造器,分别是

  1. public ReentrantLock() {
  2. sync = new NonfairSync();
  3. }
  4. public ReentrantLock(boolean fair) {
  5. sync = fair ? new FairSync() : new NonfairSync();
  6. }

3.1 Lock()原理

3.1.1 NonfairSync(非公平锁)

  1. final void lock() {
  2. if (compareAndSetState(0, 1))
  3. setExclusiveOwnerThread(Thread.currentThread());
  4. else
  5. acquire(1);
  6. }

首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
若当前有三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到了else里面。我们往下看acquire。

  1. public final void acquire(int arg) {
  2. if (!tryAcquire(arg) &&
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  4. selfInterrupt();
  5. }
  1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。
    1. tryAcquire(arg)
    2. final boolean nonfairTryAcquire(int acquires) {
    3. //获取当前线程
    4. final Thread current = Thread.currentThread();
    5. //获取state变量值
    6. int c = getState();
    7. if (c == 0) { //没有线程占用锁
    8. if (compareAndSetState(0, acquires)) {
    9. //占用锁成功,设置独占线程为当前线程
    10. setExclusiveOwnerThread(current);
    11. return true;
    12. }
    13. } else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁
    14. int nextc = c + acquires;
    15. if (nextc < 0) // overflow
    16. throw new Error("Maximum lock count exceeded");
    17. // 更新state值为新的重入次数
    18. setState(nextc);
    19. return true;
    20. }
    21. //获取锁失败
    22. return false;
    23. }
    非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。
    2. 第二步,入队。由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁死死不放,那么B和C就会被挂起。
    先看下入队的过程。先看addWaiter(Node.EXCLUSIVE)
    1. /**
    2. * 将新节点和当前线程关联并且入队列
    3. * @param mode 独占/共享
    4. * @return 新节点
    5. */
    6. private Node addWaiter(Node mode) {
    7. //初始化节点,设置关联线程和模式(独占 or 共享)
    8. Node node = new Node(Thread.currentThread(), mode);
    9. // 获取尾节点引用
    10. Node pred = tail;
    11. // 尾节点不为空,说明队列已经初始化过
    12. if (pred != null) {
    13. node.prev = pred;
    14. // 设置新节点为尾节点
    15. if (compareAndSetTail(pred, node)) {
    16. pred.next = node;
    17. return node;
    18. }
    19. }
    20. // 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
    21. enq(node);
    22. return node;
    23. }
    B、C线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设同时走到了enq(node)里。
    1. /**
    2. * 初始化队列并且入队新节点
    3. */
    4. private Node enq(final Node node) {
    5. //开始自旋
    6. for (;;) {
    7. Node t = tail;
    8. if (t == null) { // Must initialize
    9. // 如果tail为空,则新建一个head节点,并且tail指向head
    10. if (compareAndSetHead(new Node()))
    11. tail = head;
    12. } else {
    13. node.prev = t;
    14. // tail不为空,将新节点入队
    15. if (compareAndSetTail(t, node)) {
    16. t.next = node;
    17. return t;
    18. }
    19. }
    20. }
    21. }
    这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。
    当B、C入等待队列后,此时AQS队列如下:
    image.png
    3. 第三步,挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。
    1. /**
    2. * 已经入队的线程尝试获取锁
    3. */
    4. final boolean acquireQueued(final Node node, int arg) {
    5. boolean failed = true; //标记是否成功获取锁
    6. try {
    7. boolean interrupted = false; //标记线程是否被中断过
    8. for (;;) {
    9. final Node p = node.predecessor(); //获取前驱节点
    10. //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
    11. if (p == head && tryAcquire(arg)) {
    12. setHead(node); // 获取成功,将当前节点设置为head节点
    13. p.next = null; // 原head节点出队,在某个时间点被GC回收
    14. failed = false; //获取成功
    15. return interrupted; //返回是否被中断过
    16. }
    17. // 判断获取失败后是否可以挂起,若可以则挂起
    18. if (shouldParkAfterFailedAcquire(p, node) &&
    19. parkAndCheckInterrupt())
    20. // 线程若被中断,设置interrupted为true
    21. interrupted = true;
    22. }
    23. } finally {
    24. if (failed)
    25. cancelAcquire(node);
    26. }
    27. }
    再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧。 ```java /**
    • 判断当前线程获取锁失败之后是否需要挂起. */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //前驱节点的状态 int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 前驱节点状态为signal,返回true return true; // 前驱节点状态为CANCELLED if (ws > 0) { // 从队尾向前寻找第一个状态不为CANCELLED的节点 do {
      1. node.prev = pred = pred.prev;
      } while (pred.waitStatus > 0); pred.next = node; } else { // 将前驱节点的状态设置为SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

/**

  • 挂起当前线程,返回线程中断状态并重置 */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
    1. 线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。<br />整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。
    2. <a name="CElqc"></a>
    3. ### 3.1.2 FairSync公平锁
    4. 公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行aqcuire(1)<br />**超时机制**<br />在ReetrantLocktryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。那么超时的功能是怎么实现的呢?我们还是用非公平锁为例来一探究竟。
    5. ```java
    6. public boolean tryLock(long timeout, TimeUnit unit)
    7. throws InterruptedException {
    8. return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    9. }
    1. public final boolean tryAcquireNanos(int arg, long nanosTimeout)
    2. throws InterruptedException {
    3. if (Thread.interrupted())
    4. throw new InterruptedException();
    5. return tryAcquire(arg) ||
    6. doAcquireNanos(arg, nanosTimeout);
    7. }
    ```java /**
  • 在有限的时间内去竞争锁
  • @return 是否获取成功 */ private boolean doAcquireNanos(int arg, long nanosTimeout)
    1. throws InterruptedException {
    // 起始时间 long lastTime = System.nanoTime(); // 线程入队 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try {
    1. // 又是自旋!
    2. for (;;) {
    3. // 获取前驱节点
    4. final Node p = node.predecessor();
    5. // 如果前驱是头节点并且占用锁成功,则将当前节点变成头结点
    6. if (p == head && tryAcquire(arg)) {
    7. setHead(node);
    8. p.next = null; // help GC
    9. failed = false;
    10. return true;
    11. }
    12. // 如果已经超时,返回false
    13. if (nanosTimeout <= 0)
    14. return false;
    15. // 超时时间未到,且需要挂起
    16. if (shouldParkAfterFailedAcquire(p, node) &&
    17. nanosTimeout > spinForTimeoutThreshold)
    18. // 阻塞当前线程直到超时时间到期
    19. LockSupport.parkNanos(this, nanosTimeout);
    20. long now = System.nanoTime();
    21. // 更新nanosTimeout
    22. nanosTimeout -= now - lastTime;
    23. lastTime = now;
    24. if (Thread.interrupted())
    25. //相应中断
    26. throw new InterruptedException();
    27. }
    } finally {
    1. if (failed)
    2. cancelAcquire(node);
    } } ``` doAcquireNanos的流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试

    3.2 unLock()原理

    ```java public void unlock() { sync.release(1); }

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

  1. 如果理解了加锁的过程,那么解锁看起来就容易多了。流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。<br />最后我们再看下tryRelease的执行过程
  2. ```java
  3. /**
  4. * 释放当前线程占用的锁
  5. * @param releases
  6. * @return 是否释放成功
  7. */
  8. protected final boolean tryRelease(int releases) {
  9. // 计算释放后state值
  10. int c = getState() - releases;
  11. // 如果不是当前线程占用锁,那么抛出异常
  12. if (Thread.currentThread() != getExclusiveOwnerThread())
  13. throw new IllegalMonitorStateException();
  14. boolean free = false;
  15. if (c == 0) {
  16. // 锁被重入次数为0,表示释放成功
  17. free = true;
  18. // 清空独占线程
  19. setExclusiveOwnerThread(null);
  20. }
  21. // 更新state值
  22. setState(c);
  23. return free;
  24. }

这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。
用一张流程图总结一下非公平锁的获取锁的过程。
image.png