ReentrantLock与AQS源码解析

498 阅读6分钟

AQS:全称AbstractQueuedSynchronizer(抽象队列同步器),这是一个抽象类,主要作用如下

  1. 基于FIFO队列和state状态位提供了一个实现锁的框架
  2. 支持独占锁和共享锁
  3. 内部定义了ConditionObject,这是Condition接口的实现类,在使用Condition的相关功能时是必不可少的

ReentrantLock就是基于AQS实现了自己的锁机制

ReentrantLock源码解析

先看看ReentrantLock各个类的继承关系

image.png

Sync、NonfairSync、FairSync都是ReentrantLock的内部类

Sync内部类继承了AQS并重写了tryRelease方法,Sync的子类NonfairSync(非公平)和FairSync(公平)分别重写了tryAcquire方法

// 创建ReentrantLock对象时可以指定使用公平锁还是非公平锁
// 默认构造器创建非公平锁
public ReentrantLock() {
  sync = new NonfairSync();
}

// 入参true创建公平锁,入参false创建非公平锁
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}
static final class Node {
       
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        // 该节点由于超时或者中断而被取消
        static final int CANCELLED =  1;
  	// 在Node入队时, 会将前一个节点的状态置为SIGNAL,表示该节点需要被唤醒
        static final int SIGNAL = -1;
  	// 该节点目前处于condition等待队列中
        static final int CONDITION = -2;
       	// TODO:楼主也不知道这个状态位的作用
        static final int PROPAGATE = -3;
 
  	// 该节点在同步队列中的状态,初始值为0
        volatile int waitStatus;
        // 前驱节点
        volatile Node prev;
        // 后继节点
        volatile Node next;
        // 该节点的线程
        volatile Thread thread;
        // 指向下一个处于CONDITION状态的节点
        Node nextWaiter;
}

公平锁lock

从公平锁加锁的流程入手解释源码

final void lock() {
  	acquire(1);
}
public final void acquire(int arg) {
  // 这里有两个判断
  // tryAcquire:返回true表示获得锁成功,返回false表示获得锁失败。获得锁成功方法返回,不需要往下走,否则需要走加入队列的操作
  // acquireQueued(addWaiter(Node.EXCLUSIVE), arg):线程获取锁失败,走入队的逻辑
  if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

AQS中的钩子方法,子类必须实现此方法,否则抛出异常

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
	// 公平锁 
	static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
      	
      	// 重写AQS中的模版方法
        protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                // 获得state的值
                int c = getState();
          	// state为0,说明该节点处于初始状态
                if (c == 0) {
                    // hasQueuedPredecessors 返回false表示等待队列中没有等待的线程
                    // compareAndSetState CAS state 成功从0变为1,成功上锁,保证同一时间只有一个线程争抢到锁
                    // 争抢失败走入队操作
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        // 设置一个排它线程
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                // ReentrantLock 是一把可重入锁,也就是说同一个线程可以重复上锁
                // 这里先判断是不是同一个线程
                else if (current == getExclusiveOwnerThread()) {
                    // state + 1
                    int nextc = c + acquires;
                    // 重复上锁到达上限
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                // false表示线程没有获取到锁
                return false;
            }
        }
		}
// 返回true表示阻塞队列中已经有等待的队列
// 返回false表示阻塞队列中没有等待的队列
public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
      ((s = h.next) == null || s.thread != Thread.currentThread());
}

h != t &&((s = h.next) == null || s.thread != Thread.currentThread());

这个判断是否成立要分三种情况来说:

  1. 队列未初始化
  2. 队列已初始化,但是队列中只有一个节点
  3. 队列已初始化,队列中超过一个节点

第一种情况:队列未初始化,tail、head都为null,h != t不成立,直接返回false

第二种情况:队列中只有一个节点,这个节点是哨兵空节点,tail、head都指向了同一个节点,h != t不成立,直接返回false

第三种情况:队列中有多个节点,h != t(成立),h.next不为null(不成立),s.thread != Thread.currentThread()如果当前获取锁的线程与节点中的线程一致,返回false,不一致则返回true

// AQS队列中的头节点
private transient volatile Node head;
// AQS队列中的尾节点
private transient volatile Node tail;


private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // pred 设值为 tail
    Node pred = tail;
    // FIFO队列入队要分两种情况,队列已初始化,队列未初始化
    // 队列未初始化的情况下,pred为null,走enq(node)的方法
    // 队列已初始化的情况下,相当于队列后新增节点,走if这个方法
    if (pred != null) {
      node.prev = pred;
      // 这里如果CAS失败了,enq中也有一个CAS到队尾的操作,并且是for死循环的
      if (compareAndSetTail(pred, node)) {
        pred.next = node;
        return node;
      }
    }
    enq(node);
    return node;
}

// 此方法可见下图
private Node enq(final Node node) {
    // 死循环
    for (;;) {
      Node t = tail;
      // tail初始化状态下为null,所以第一次循环会先走if
      if (t == null) { // Must initialize
        // 先设置一个哨兵节点(哨兵节点是一个空节点)
        if (compareAndSetHead(new Node()))
          // 头节点和尾节点都指向这个哨兵节点
          tail = head;
      // 第二次循环走else
      } else {
        node.prev = t;
        if (compareAndSetTail(t, node)) {
          // 队列后新增节点
          t.next = node;
          return t;
        }
      }
    }
}

image.png

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
      // 标志该线程是否在等待时被中断了
      boolean interrupted = false;
      for (;;) {
        // 获取该节点的前驱节点
        final Node p = node.predecessor();
        // 这里是两个判断
        // p == head:如果前驱节点是头节点,说明当前线程是第一个在等待的线程(上文说过头节点是一个空节点)
        // tryAcquire(arg):在当前线程是第一个在等待的线程的情况下,会再去尝试获取锁,相当于做了一次自旋操作
        // 如果自旋获取锁成功,当前节点会变成头节点
        // 如果当前线程不是第一个在等待的线程,不会做自旋操作
        if (p == head && tryAcquire(arg)) {
          setHead(node);
          p.next = null; // help GC
          failed = false;
          // 理论上这里是for死循环唯一的出口
          return interrupted;
        }
        // 当前线程不是第一个在等待的线程,会park进行阻塞状态
        // shouldParkAfterFailedAcquire返回true的情况下才会执行parkAndCheckInterrupt这个方法对该节点进行park
        if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
          // 走到这里表示线程已被中断
          interrupted = true;
      }
    } finally {
      if (failed)
        cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  	// 获取前驱节点的状态
        int ws = pred.waitStatus;
  	// 前驱节点的状态位已经是-1,不做处理
        if (ws == Node.SIGNAL)
            return true;
  	// 如果前驱节点的状态位>0,表示前驱节点可能中断或者超时了,此处扔弃cancelled状态的节点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 将前驱节点的状态设置为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
 }

这个方法的主要作用是确保当前节点的前驱节点的状态值为SIGNAL

p是node的前驱节点,判断一个节点是否需要被阻塞是通过该节点的前驱节点的状态判断的,这个方法中有三种情况

  1. 前驱节点状态为singal,表示前驱节点还在等待,当前节点需要继续被阻塞。返回true
  2. 前驱节点大于0,则表示前驱节点可能中断或者超时了,扔弃cancelled状态的节点
  3. 前驱节点为其他状态(0,PROPAGATE),表示当前节点需要重试尝试获取锁。返回false,方法外层是一个for死循环,会再进行一次自旋获取锁的操作
private final boolean parkAndCheckInterrupt() {
    // 将线程阻塞
    LockSupport.park(this);
    // 检测该线程是否已被中断,如果已被中断,返回true
    return Thread.interrupted();
}

加锁流程图

image.png

公平锁与非公平锁unlock

从释放锁流程入手解释源码

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;
}
protected final boolean tryRelease(int releases) {
    // ReentrantLock是可重入的,这里将state-1
    int c = getState() - releases;
    // 判断当前的线程是否是排它锁中的线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
      throw new IllegalMonitorStateException();
    boolean free = false;
    // c为0表示没有线程持有这把锁
    if (c == 0) {
      free = true;
      setExclusiveOwnerThread(null);
    }
    setState(c);
    // free为true表示没有线程持有这把锁
    // free为false表示还有线程持有这把锁
    return free;
}
private void unparkSuccessor(Node node) {
		
    int ws = node.waitStatus;
    if (ws < 0)
      compareAndSetWaitStatus(node, ws, 0);

    // 获取当前节点的后继节点(当前节点为头节点)
    Node s = node.next;
    // 如果后继节点为空,或者是cancelled状态
    // 这里s==null并不代表s就是tail,此时可能有新节点入队,并处在完成了cas但没有更新next指针 这一时刻
    if (s == null || s.waitStatus > 0) {
      s = null;
      // 从tail开始向前遍历, 找到状态不为CANCELLED的节点
      // 为啥从后往前 -> 在addWaiter与enq方法中, 入队并不是一个原子操作(分为三步1设置prev指针,2设置tail指针,3设置next							// 指针)。
      // 所以从前往后遍历, 可能会漏掉节点, 因为此时next指针可能为空。
      for (Node t = tail; t != null && t != node; t = t.prev)
        if (t.waitStatus <= 0)
          s = t;
    }
    // 唤醒阻塞节点
    if (s != null)
      LockSupport.unpark(s.thread);
}

非公平锁的lock

abstract static class Sync extends AbstractQueuedSynchronizer {
        
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
              	// 与公平锁差异点在于不判断等待队列中是否有等待线程,直接上锁
                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;
        }
}

总结

对于ReentrantLock来说,其执行逻辑如下:

  1. 尝试获取对象的锁,如果获取不到,非公平锁跟公平锁有不同的处理

    • 公平锁会直接进入阻塞队列的末尾,

    • 非公平锁会先进行CAS,尝试获取锁,获取失败则与公平锁的处理方式一致,进入阻塞队列的末尾

  2. 获取到了,会判断之前是否已经获取过一次来决定是否进行重入

  3. 当锁被释放时,会对state成员变量进行减1操作,如果减1后,state值不为0,那么release操作就执行完毕;如果减1操作后,state值为0,则调用LockSupport的unpark方法唤醒该线程后的等待队列中的第一个后继线程,将其唤醒,使之能够获取到对象的锁(公平锁与非公平锁的处理逻辑是一致的)

Debug代码

开启两个线程,一个线程获取锁,可以查看到另外一个线程阻塞的过程

public class ReentrantLockTest {

    private final Lock reentrantLock = new ReentrantLock(true);

    private void method() {

        try {
            reentrantLock.lock();

            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("method");
        } finally {
            reentrantLock.unlock();
        }
    }

    public static void main(String[] args) {

        ReentrantLockTest reentrantLockTest = new ReentrantLockTest();

        Thread t1 = new Thread(reentrantLockTest::method);
        Thread t2 = new Thread(reentrantLockTest::method);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}