从ReentrantLock详解AQS原理源码解析

Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。本文旨在从ReentrantLock详解AQS原理源码解析。java

数据结构


java.util.concurrent.locks.AbstractQueuedSynchronizer类中存在以下数据结构。node

// 链表结点
static final class Node {}

// head指向的是一个虚拟结点,刷多了算法就知道这样作的目的是方便对链表操做,真正的头为head.next
private transient volatile Node head;

// 尾结点
private transient volatile Node tail;

// 同步状态,用于展现当前临界资源的获锁状况。
private volatile int state;

// 继承至AbstractOwnableSynchronizer类
// 独占模式下当前锁的拥有者
private transient Thread exclusiveOwnerThread;

// 自旋锁的自旋纳秒数,用于提升应用的响应能力
static final long spinForTimeoutThreshold = 1000L;

// unsafe类
private static final Unsafe unsafe = Unsafe.getUnsafe();

// 如下字段对应上面字段的在对象中的偏移值,在静态代码块中初始化,其值是相对于在这个类对象中的偏移量
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

在AQS类中内部类Node包含以下数据结构算法

static final class Node {

	// 共享锁
	static final Node SHARED = new Node();

	// 独占锁
    static final Node EXCLUSIVE = null;
       
    // 0	               当一个Node被初始化的时候的默认值
	// CANCELLED	为  1,表示线程获取锁的请求已经取消了
	// CONDITION	为 -2,表示节点在等待队列中,节点线程等待唤醒
	// PROPAGATE	为 -3,当前线程处在SHARED状况下,该字段才会使用
	// SIGNAL	    为 -1,表示线程已经准备好了,就等资源释放了
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    
    // 前驱指针
    volatile Node prev;
    
    // 后继指针
    volatile Node next;
	
	// 该节点表明的线程对象
    volatile Thread thread;

    Node nextWaiter;
}

从其数据结构能够猜想出c#

  • AQS类中主要的存储结构应该是一个双向链表。
  • state字段对应了这个锁对象的状态。
  • 线程申请锁时会将其包装成一个节点。Node保存了获取锁的线程信息。
  • Node.waitStatus字段保存这个线程申请锁的状态。
  • head指向的是一个虚拟结点,真正有效的头为head.next

源码分析


咱们从AQS的实现类ReentrantLock#lock开始分析其具体的流程。后端

ReentrantLock#lock

public void lock() {
    sync.lock();
}

直接调用了Sync类的lock()方法,Sync类在ReentrantLock中有两个实现类分别是FairSync和NonfairSync,分别对应了公平锁和非公平锁。安全

  • 公平锁:线程获取锁的顺序和调用lock的顺序同样,FIFO;
  • 非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。

因为ReentrantLock默认是非公平锁,咱们从NonfairSync类分析。数据结构

ReentrantLock.NonfairSync#lock

final void lock() {
	// cas操做尝试将state字段值修改成1
    if (compareAndSetState(0, 1))
    	// 成功的话就表明已经获取到锁,修改独占模式下当前锁的拥有者为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	// 获取锁失败以后的操做
        acquire(1);
}

从这能够肯定咱们以前的猜想框架

  • state字段对应了这个锁对象的状态,值为0的时候表明锁没有被线程占用,修改成1以后表明锁被占用。

如今分析未获取到锁以后的流程jvm

AbstractQueuedSynchronizer#acquire

public final void acquire(int arg) {
	
    if (
    		// 当前线程尝试获取锁
    		!tryAcquire(arg) &&
    		// acquireQueued会把传入的结点在队列中不断去获取锁,直到获取成功或者再也不须要获取(中断)。
        	acquireQueued(
        		// 在双向链表的尾部建立一个结点,值为当前线程和传入的模式
	        	addWaiter(Node.EXCLUSIVE), 
	        	arg
        	)
        )
        // TODO
        selfInterrupt();
}

看不懂,先查找资料了解这几个方法的做用,注释在代码中。函数

ReentrantLock.NonfairSync#tryAcquire

// 当前线程尝试获取锁
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

ReentrantLock.Sync#nonfairTryAcquire

// 当前线程尝试获取锁-非公平
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 得到当前锁对象的状态
    int c = getState();
    // state为0表明当前没有被线程占用
    if (c == 0) {
    	// cas操做尝试将state字段值修改成请求的数量
        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");
        // state值增长相应的请求数。
        setState(nextc);
        return true;
    }
    return false;
}

ReentrantLock字面意思是可重入锁

  • 可重入锁:一个线程在获取一个锁以后,在没有释放以前仍然能够继续申请锁而不会形成阻塞,可是解锁的时候也须要相应次数的解锁操做。

结合nonfairTryAcquire方法逻辑,能够推断出state字段在独占锁模式下还表明了锁的重入次数。

AbstractQueuedSynchronizer#addWaiter

// 在链表尾部建立一个结点,值为当前线程和传入的模式
private Node addWaiter(Node mode) {
	// 建立一个结点,值为当前线程和传入的模式
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 快速路径,是为了方便JIT优化。jvm检测到热点代码,会将其编译成本地机器码并以各类手段进行代码优化。
    Node pred = tail;
    if (pred != null) {
    	// 将新建立的node的前驱指针指向tail。
        node.prev = pred;
        // 将结点修改成队列的tail时可能会发生数据冲突,用cas操做保证线程安全。
        if (compareAndSetTail(pred, node)) {
        	// compareAndSetTail比较的地址,若是相等则将新的地址赋给该字段(而不是在源地址上替换,为何我会这么想???)
        	// 因此此处pred引用指向的仍然是源tail的内存地址。将其后继指针指向新的tail
            pred.next = node;
            return node;
        }
    }
    // 队列为空或者cas失败(说明被别的线程已经修改)
    enq(node);
    return node;
}

这个方法主要做用是在链表尾部建立一个结点,返回新建立的结点,其主要流程为

  • 经过当前的线程和锁模式建立一个节点。
  • 节点入尾操做
    • 新节点的前驱指针指向tail
    • 使用cas操做修改新节点为tail
    • 原tail的后继指针指向新节点

当队列为空或者cas失败(说明被别的线程已经修改)会执行enq方法兜底。

AbstractQueuedSynchronizer#enq

// 在队列尾部建立一个结点,值为当前线程和传入的模式,当队列为空的时候初始化。
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
        	// 建立一个空结点设置为头,真正的头为hdead.next
            if (compareAndSetHead(new Node()))
            	// 尾等于头
                tail = head;
        } else {
        	// 这段逻辑跟addWaiter()中快速路径的逻辑同样。
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

addWaiter是对enq方法的一层封装,addWaiter首先尝试一个快速路径的在链表尾部建立一个结点,失败的时候回转入enq方法兜底,循环在链表尾部建立一个节点,直到成功为止。

这里有个疑问,为何要在addWaiter方法中尝试一次在enq方法中能完成的在链表尾部建立一个节点的操做呢?实际上是为了方便JIT优化。jvm检测到热点代码,会将其编译成本地机器码并以各类手段进行代码优化。了解更多1了解更多2

在链表尾插入须要

AbstractQueuedSynchronizer#acquireQueued

// acquireQueued会把传入的结点在队列中不断去获取锁,直到获取成功或者再也不须要获取(中断)。
final boolean acquireQueued(final Node node, int arg) {
	// 标记是否成功拿到锁
    boolean failed = true;
    try {
    	// 标记获取锁的过程当中是否中断过
        boolean interrupted = false;
        // 开始自旋,要么获取锁,要么中断
        for (;;) {
        	// 得到其前驱节点
            final Node p = node.predecessor();
            // 若是前驱节点为head表明如今节点node在队列有效数据的第一位,就尝试获取锁
            if (p == head && tryAcquire(arg)) {
            	// 获取锁成功,把当前节点置为虚节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 若是存在如下状况就要判断当前node是否要被阻塞
            // 1. p为头节点且获取锁失败 2. p不为头结点
            if (shouldParkAfterFailedAcquire(p, node) &&
            	// 阻塞进程
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
        	// 取消申请锁
            cancelAcquire(node);
    }
}

AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire

// 依赖前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 入参请求锁的node的前驱节点的状态
    int ws = pred.waitStatus;
    // 若是前驱节点的状态为"表示线程已经准备好了,就等资源释放了"
    // 说明前驱节点处于激活状态,入参node节点须要被阻塞
    if (ws == Node.SIGNAL)
        return true;
    // 只有CANCELLED状态对应大于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;
}

ReentrantLock#lock总结

到如今咱们能够总结一下ReentrantLock#lock非公平锁方法的流程

未获取到锁的状况下函数调用流程

  • ReentrantLock#lock
  • ReentrantLock.Sync#lock
  • ReentrantLock.NonfairSync#lock
  • AbstractQueuedSynchronizer#acquire
  • ReentrantLock.NonfairSync#tryAcquire
  • ReentrantLock.Sync#nonfairTryAcquire
  • AbstractQueuedSynchronizer#addWaiter
  • AbstractQueuedSynchronizer#acquireQueued

描述

  • 执行ReentrantLock的Lock方法。
  • 会调用到内部类Sync的Lock方法,因为Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的Lock方法,cas修改state值获取锁,失败执行父类的Acquire方法。
  • 父类的Acquire方法会执行子类实现的tryAcquire方法,由于tryAcquire须要自定义同步器实现,所以执行了ReentrantLock中的tryAcquire方法,因为ReentrantLock是经过公平锁和非公平锁内部类实现的tryAcquire方法,所以会根据锁类型不一样,执行不一样的tryAcquire。
  • tryAcquire是获取锁逻辑,获取失败后,会执行框架AQS的后续逻辑,跟ReentrantLock自定义同步器无关。
// 公平锁加锁时判断等待队列中是否存在有效节点的方法。
// 返回False,当前线程能够争取共享资源;
// 返回True,队列中存在有效节点,当前线程必须加入到等待队列中。
public final boolean hasQueuedPredecessors() {
	Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    // 头不等于尾表明队列中存在结点返回true
    // 可是还有一种特例,就是若是如今正在执行enq方法进行队列初始化,tail = head;语句运行以后
    // 此时h == t,返回false,可是队列中
    return h != t &&
    	// 从这能够看出真正的头结点是head.next,即说明head是一个无实际数据的结点,为了方便链表操做
        ((s = h.next) == null 
        // 有效头结点与当前线程不一样,返回true必须加入到等待队列
        || s.thread != Thread.currentThread());
}

即时编译器

Java程序最初都是经过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提升热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各类手段尽量地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。
这里所说的热点代码主要包括两类

  • 被屡次调用的方法
  • 被屡次执行的循环体

对于这两种状况,编译的目标对象都是整个方法体,而不会是单独的循环体

未完待续

相关文章
相关标签/搜索