ReentrantLock原理

AQS:AbstractQueuedSynchronizer,是一个依赖状态的同步器,定义了一套多线程访问共享资源的同步器框架。对于等待队列、条件队列、独占共享等行为进行一系列抽象。
ReentrantLock是一种基于AQS框架的实现,作用类似于synchronized,是一种互斥锁,且它具有比synchronized更多的特性,支持手动加锁和解锁,支持公平锁和非公平锁。
先看ReentrantLock的简单应用:
// false为非公平锁,true为公平锁,默认是公平锁
ReentrantLock lock = new ReentrantLock(true);
// 加锁
lock.lock() 
// 具体的业务逻辑
// 解锁
lock.unlock() 

我们进入到这个ReentrantLock这个类,ReentrantLock作为AQS的应用实现,通过内部类Sync继承AbstractQueuedSynchronizer,这个类尤其重要,是整个juc抽象的核心。

// 构造器,默认创建的是公平锁
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

FairSync和NonfairSync都是Sync的子类,而Sync继承了AbstractQueuedSynchronizer,先简单来看AbstractQueuedSynchronizer中的重要属性:

// 头节点
private transient volatile Node head;

// 尾节点
private transient volatile Node tail;

// 状态,AQS是基于状态的同步器,用的就是这个状态值
private volatile int state;  

// 同步队列使用双向链表来构建,使用内部类Node创建节点
static final class Node {
  // 标记共享模式
  static final Node SHARED = new Node();
  // 标记独占模式 
  static final Node EXCLUSIVE = null;
  // 表示出现异常,可能是中断,也可能是执行出现错误,需要取消等待
  static final int CANCELLED =  1;
  // 表示可被唤醒
  static final int SIGNAL    = -1;
  // 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
  static final int CONDITION = -2;
  // 表示下一次共享式同步状态获取将会被无条件地传播下去
  static final int PROPAGATE = -3;
  // 节点状态
  volatile int waitStatus;
  // 前驱节点
  volatile Node prev;
  // 后继节点  
  volatile Node next;
  // 节点对应的线程
  volatile Thread thread;
  // 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量
  Node nextWaiter;
  // 空节点,用于标记共享模式
  Node() {
  }
  // 用于同步队列
  Node(Thread thread, Node mode) {
    this.nextWaiter = mode;
    this.thread = thread;
  }
  // 用于条件队列
  Node(Thread thread, int waitStatus) { 
    this.waitStatus = waitStatus;
    this.thread = thread;
  }
}

再回到ReentrantLock中,加锁的方法为Lock,调用的具体实现为FairSync的Lock():

final void lock() {
  // 调用AQS中的acquire方法
  acquire(1);
}

// AQS中的acquire方法
public final void acquire(int arg) {
  // 先尝试获得锁,如果不能成功,进行队列入队
  if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
}

// tryAcquire调用的是子类具体实现:FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
  // 获取当前线程
  final Thread current = Thread.currentThread();
  // 获取状态值
  int c = getState();
  // 如果为0,说明现在没有线程获得锁,尝试加锁
  if (c == 0) {
    // hasQueuedPrecessors是判断是否需要等待,如果不需要,对state进行cas
    if (!hasQueuedPredecessors() && compareAndSetState(0,acquires)) {
      // 默认是独占锁,将独占的线程设置为当前线程
      setExclusiveOwnerThread(current);
      return true;
    }
    }
  // 已经有线程加锁,判断获得锁的线程是否是当前线程
  else if (current == getExclusiveOwnerThread()) {
      int nextc = c + acquires;
      if (nextc < 0) throw new Error("Maximum lock count exceeded");
      // 这里就是可重入的表现,不需要进行cas,因为当前线程已经获取过一次,不可能有其他线程拿到当前锁。多次加锁未来就会多次解锁。
      setState(nextc);
      return true;
  }
  // 未能成功加锁
  return false;
}
/**
 * 入队
 * @param mode  表示的是锁的模式:独占或者共享,用Node节点表示,在Node注释中有提到。
 */
private Node addWaiter(Node mode) {
  // 构造一个节点,构造参数中传入当前线程和锁模式,
  Node node = new Node(Thread.currentThread(), mode);
  // 把尾节点赋值给pred,当然,此时的tail有可能是为空的,因为之前有可能还没有初始化等待队列
  Node pred = tail;
  if (pred != null) {
    node.prev = pred;
    if (compareAndSetTail(pred, node)) {
      pred.next = node;
      return node;
    }
  }
  enq(node);
  return node;
}
// 节点入队
private Node enq(final Node node) {
  // 进入自旋,目的是保证入队一定能成功。因为此时可能有多个节点来入队,一个节点一次入队不一定成功。
  for (;;) {
    Node t = tail;
    if (t == null) {
      // 在AQS的设计思想中,构建队列之前一定保证了队列初始化过。头部放一个空节点,内部的线程属性不指向任何线程。队头和队尾同时指定这个空节点,这样队列初始化完成。
      if (compareAndSetHead(new Node()))
        tail = head;
    } else {
      node.prev = t;
      // 将当前节点入队,当前节点就变成了队列的尾节点。此处也存在竞争,为了保证所有阻塞线程对象能被唤醒,所以要保证安全入队。
      if (compareAndSetTail(t, node)) {
        // 之前的尾部指针的next指向新节点
        t.next = node;
        return t;
      }
    }
  }
}
// 节点入队之后的处理。主要进行节点阻塞
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;
        return interrupted;
      }
      // 尝试获取锁失败,判断是否需要阻塞
      // 在这个方法中会调用LockSupport.park(this),进行线程阻塞
      if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
        interrupted = true;
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}
// 获取锁失败,是否需要阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   // 前驱节点的状态
  int ws = pred.waitStatus;
  // 前驱节点的状态是SIGNAL,表示当前节点是可被唤醒的。这个地方的设计比较奇特,使用前驱节点的状态来标记唤醒当前节点。A->B->C,A节点的状态表示B能否被唤醒
  if (ws == Node.SIGNAL)
    return true;
  // 前驱节点的状态大于0,我们认为出现错误,将当前节点的前驱节点,和前驱节点都进行前移,直到前驱节点的状态正常
  if (ws > 0) {
    do {
      node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
  } else {
    // 将前驱节点的状态修改为SIGNAL,下次可唤醒
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  }
  return false;
}

到此,加锁的代码处理完毕。未获得锁的线程是阻塞的,如何被唤醒?当然是在unlock方法,当前获取锁的线程释放锁之后,就会去唤醒下一个线程,unlock代码跟下去:

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) {   // 状态减   int c = getState() - releases;   if (Thread.currentThread() != getExclusiveOwnerThread())     throw new IllegalMonitorStateException();   boolean free = false;   if (c == 0) {     free = true;     // 把当前AQS独占的线程置空     setExclusiveOwnerThread(null);   }   setState(c);   return free; } // 线程唤醒 private void unparkSuccessor(Node node) {   int ws = node.waitStatus;   if (ws < 0)     // 再次将节点状态改为0     compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; }   // 线程唤醒 if (s != null) LockSupport.unpark(s.thread); }

因为Doug Lea将太多的逻辑集成到AQS中,这个代码深入到每一行几乎时读不下去的,重点是学习他的设计思路,多线程之间互相阻塞和唤醒。 

 
 
 

posted @ 2020-11-23 20:07  以战止殇  阅读(62)  评论(0编辑  收藏  举报