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的实现类ReentrantLock#lock开始分析其具体的流程。后端
public void lock() { sync.lock(); }
直接调用了Sync类的lock()方法,Sync类在ReentrantLock中有两个实现类分别是FairSync和NonfairSync,分别对应了公平锁和非公平锁。安全
因为ReentrantLock默认是非公平锁,咱们从NonfairSync类分析。数据结构
final void lock() { // cas操做尝试将state字段值修改成1 if (compareAndSetState(0, 1)) // 成功的话就表明已经获取到锁,修改独占模式下当前锁的拥有者为当前线程 setExclusiveOwnerThread(Thread.currentThread()); else // 获取锁失败以后的操做 acquire(1); }
从这能够肯定咱们以前的猜想框架
如今分析未获取到锁以后的流程jvm
public final void acquire(int arg) { if ( // 当前线程尝试获取锁 !tryAcquire(arg) && // acquireQueued会把传入的结点在队列中不断去获取锁,直到获取成功或者再也不须要获取(中断)。 acquireQueued( // 在双向链表的尾部建立一个结点,值为当前线程和传入的模式 addWaiter(Node.EXCLUSIVE), arg ) ) // TODO selfInterrupt(); }
看不懂,先查找资料了解这几个方法的做用,注释在代码中。函数
// 当前线程尝试获取锁 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
// 当前线程尝试获取锁-非公平 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字段在独占锁模式下还表明了锁的重入次数。
// 在链表尾部建立一个结点,值为当前线程和传入的模式 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; }
这个方法主要做用是在链表尾部建立一个结点,返回新建立的结点,其主要流程为
当队列为空或者cas失败(说明被别的线程已经修改)会执行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。
在链表尾插入须要
// 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); } }
// 依赖前驱节点判断当前线程是否应该被阻塞 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非公平锁方法的流程
未获取到锁的状况下函数调用流程
描述
// 公平锁加锁时判断等待队列中是否存在有效节点的方法。 // 返回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),为了提升热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各类手段尽量地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。
这里所说的热点代码主要包括两类
对于这两种状况,编译的目标对象都是整个方法体,而不会是单独的循环体