JUC ReentrantLock 原理

占用资源成功,没有竞争的情况

  1. 入口是 java.util.concurrent.locks.ReentrantLock#lock

    public void lock() {
        sync.lock();
    }
    
    • 可以知道调用了 sync 属性的 lock 方法

    • sync 属性在创建 ReentrantLock 就确定了是公平锁还是非公平锁

    • 这里分析公平锁,所以 sync 会调用 FairSync 的 lock 方法

  2. java.util.concurrent.locks.ReentrantLock.FairSync#lock

    static final class FairSync extends Sync {
        ...
        final void lock() {
            acquire(1);
        }
    	...
    }
    
    • FairSync.lock 调用 acquire 方法,但是 FairSync 没有 acquire,所以要调用继承来的方法
    • 直接父类 Sync 也没有 acquire,爷爷类 AQS 有,所以继续看 AQS 的 acquire 做了什么
  3. java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire

    public final void acquire(int arg) {
        // tryAcquire 是抢占资源(注意这里是取反,当抢占失败才会走后面的逻辑即加入队列,抢占成功就没后面什么事儿了)
        if (!tryAcquire(arg) &&
            // acquireQueued 是把当前线程加入队列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    

    这里要分析两点,1 怎么抢占资源的?2 抢占失败,怎么加入队列的?先看怎么抢占资源,也就是 tryAcquire 逻辑

  4. java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquire

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    • tryAcquire 方法直接抛一个异常?是想告诉我们这个方法必须子类实现(面向对象知识:子类重写父类方法,会调用子类的)
    • 典型的模板设计模式
    • FairSync 继承 Sync 继承 AQS,这里在 AQS 中调用本类方法,但是子类重写了,所以会调用子类重写过的方法,也就是 FairSync.tryAcquire()
  5. java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire

    // 又回到了 FairSync 中
    static final class FairSync extends Sync {
        ...
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取资源状态,volatile 修饰的 state,也是来自于 AQS
            int c = getState();
            // state 为 0 则代表当前资源空闲
            if (c == 0) {
                // 资源空闲为什么不立马占用?还要再执行 hasQueuedPredecessors?
                if (!hasQueuedPredecessors() &&
                    // cas 修改 state
                    compareAndSetState(0, acquires)) {
                    // 设置占用线程为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // state 不为 0 代表当前资源已被占用,当被占用不是直接返回 false,而是判断当前线程是否就是占用资源的线程(重入锁)
            else if (current == getExclusiveOwnerThread()) {
                // 重入锁:state = state +1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 返回 false,这时才会执行第 3 步的 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法
            return false;
        }
        ...
    }
    
    • 重点关注下 state == 0 的情况,当前资源为 0 意味着资源空闲,既然空闲为什么不立马占用资源,还要再判断呢?因为这是公平锁,要看是否需要排队

    • 如果是非公平锁,两个地方体现了插队

      # java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
      static final class NonfairSync extends Sync {
          ...
          // 一来就 cas,失败再进入 acrquire 方法
          final void lock() {
              if (compareAndSetState(0, 1))
                  setExclusiveOwnerThread(Thread.currentThread());
              else
                  acquire(1);
          }
      	...
      }
      
      # java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
      final boolean nonfairTryAcquire(int acquires) {
          final Thread current = Thread.currentThread();
          int c = getState();
          if (c == 0) {
              // 只要资源空闲,再次直接 cas,不会考虑队列时候已经有线程在排队了
              if (compareAndSetState(0, acquires)) {
                  setExclusiveOwnerThread(current);
                  return true;
              }
          }
          else if (current == getExclusiveOwnerThread()) {
              int nextc = c + acquires;
              if (nextc < 0) // overflow
                  throw new Error("Maximum lock count exceeded");
              setState(nextc);
              return true;
          }
          return false;
      }
      
  6. tryAcquire 返回 true 说明占用资源成功,流程结束

    • 两种情况占用资源成功:1 非公平锁插队成功;2 公平锁此时队列为空,当前线程直接占用资源
    • 占用失败的情况还没有分析,占用失败要进行入队、挂起等操作

占用资源失败,加锁失败,处理入队、挂起

  1. state != 0,并且当前线程不是占有资源的线程
  2. 公平锁场景,state = 0,但是现在已经有线程在排队
  3. 非公平锁场景,state = 0,占用失败即插队失败
    • cas 是虽然原子的,但是正准备 cas 还没有 cas 的时候,刚好别的线程被唤醒占用了资源

以上三种情况都会导致占用失败,一旦失败就会进行入队、挂起操作,源码如下:

public final void acquire(int arg) {
    // tryAcquire 是子类方法,如果返回 false,说明占用资源失败
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

只要占用资源失败就继续判断 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 条件,两个方法分别看

  1. java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
private Node addWaiter(Node mode) {
  // 当前线程包装成节点(mode:独占 or 共享)
  Node node = new Node(Thread.currentThread(), mode);
  // 尾节点
  Node pred = tail;
  // 尾节点不为空,说明队列已经初始化过了,直接入队
  if (pred != null) {
      node.prev = pred;
      // 设置新的节点为队尾节点
      if (compareAndSetTail(pred, node)) {
          pred.next = node;
          return node;
      }
  }
  // 初始化队列,并入队
  enq(node);
  return node;
}
  1. java.util.concurrent.locks.AbstractQueuedSynchronizer#enq
private Node enq(final Node node) {
  // 死循环
  for (;;) {
      Node t = tail;
      // 第一次循环,t 应该是 null,表示队列为空,先初始化
      if (t == null) {
          // 当前节点设置为头节点(头节点只是new了一个对象,对象的属性都是 null!)
          if (compareAndSetHead(new Node()))
              tail = head; // 尾节点也指向头节点(这一次循环就结束了)
      } else {
          // 第二次循环,队列已经不为空了,维护链表的指针(就是维护当前节点上一个和后一个节点)
          node.prev = t;
          if (compareAndSetTail(t, node)) {
              t.next = node;
              return t;
          }
      }
  }
}

这是一个死循环,第一次循环 t 肯定是 null,表示队列为空,需要进行初始化,分为两次循环完成

  • 第一次循环初始化队列,给队列设置头尾节点

  • 第二次循环维护节点的指针,就是把节点维护成一个链表

  • 可能不止两次循环,当 cas 失败就会多次循环,反正是个死循环,只有维护好队列才会结束

  1. java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 节点的前置节点
                final Node p = node.predecessor();
                // 如果前置节点是头节点,说明当前节点是排队的第一个(因为头节点是正在处理,正在处理的不用排队)
                // 如果当前节点是排队的第一个就抢锁(不是第一个就等着吧,挨着来)
                if (p == head && tryAcquire(arg)) {
                    // 抢锁成功表示正在处理的线程运行结束,当前节点成为新的头节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 返回到 acquire 方法中,表示获取了锁,不会中断
                    return interrupted;
                }
                // 执行到这里两种情况,1:当前节点不是排队的第一个;2:当前节点是排队的第一个但抢锁失败
                // shouldParkAfterFailedAcquire 计算在获取锁失败后是否需要阻塞线程
                // parkAndCheckInterrupt 阻塞并且检查中断状态
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 走到线程挂起就暂停了,不会继续执行,但是循环还未结束!等到当前节点被唤醒
                    // 唤醒后设置 interrupted = true,并不会返回到 acquire 中,而是继续循环
                    interrupted = true;
            }
        } finally {
            // 基本不会走到这一步,只是更严谨一点
            if (failed)
                cancelAcquire(node);
        }
    }
    

释放资源,释放锁、唤醒排队的节点

没看到出队操作,后续再研究下

和 synchronized 别搞混了,ReentrantLock 可以重入,但也必须要使用 lock 方法来获取锁,调用了几次 lock 就要调用几次 unlock,意味着重复锁会调用多次 unlock

  1. 入口是 java.util.concurrent.locks.ReentrantLock#unlock

    public void unlock() {
        // 同加锁一致,sync 是 NonfairSync 或 FairSync
        // 但是 NonfairSync 和 FairSync 并没有 release 方法,所以会调用父类的方法(直接父类 Sync 也没有,于是调用 AQS 的)
        sync.release(1);
    }
    
  2. java.util.concurrent.locks.AbstractQueuedSynchronizer#release

    public final boolean release(int arg) {
        if (tryRelease(arg)) { // 释放锁
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); // 唤醒下一个节点的线程
            return true;
        }
        return false;
    }
    
    • 调用 tryRelease 方法来释放锁(把 state 置为 0 并设置占用线程为 null)
    • 释放成功就唤醒排队的线程
  3. java.util.concurrent.locks.AbstractQueuedSynchronizer#tryRelease

    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    

    tryAcquire 方法一致,也是一个模板方法,调用子类 SynctryAcquire 方法

  4. java.util.concurrent.locks.ReentrantLock.Sync#tryRelease

    protected final boolean tryRelease(int releases) {
        // 获取 state 的值,减去 release(值是1)
        int c = getState() - releases;
        // 如果当前线程不是持有资源的线程,这不扯犊子吗,直接抛一个异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 如果 c 是 0
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        // cas 设置 state 的值为 0
        setState(c);
        return free;
    }
    
  5. java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor

    private void unparkSuccessor(Node node) {
    	...
        // 下一个节点
        Node s = node.next;
    	...
        // 唤醒下一个节点的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    
  6. 流程结束

posted @ 2023-07-12 10:38  CyrusHuang  阅读(3)  评论(0编辑  收藏  举报