Java的内置锁一直都是备受争议的,在JDK 1.6以前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,可是与Lock相比synchronized仍是存在一些缺陷的:虽然synchronized提供了便捷性的隐式获取锁释放锁机制(基于JVM机制),可是它却缺乏了获取锁与释放锁的可操做性,可中断、超时获取锁,且它为独占式在高并发场景下性能大打折扣。node
在介绍Lock以前,咱们须要先熟悉一个很是重要的组件,掌握了该组件JUC包下面不少问题都不在是问题了。该组件就是AQS。安全
AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其余同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的做者(Doug Lea)指望它可以成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。数据结构
AQS解决了子啊实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器能够带来不少好处。它不只可以极大地减小实现工做,并且也没必要处理在多个位置上发生的竞争问题。并发
在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而下降上下文切换的开销,提升了吞吐量。同时在设计AQS时充分考虑了可伸缩行,所以J.U.C中全部基于AQS构建的同步器都可以得到这个优点。框架
AQS的主要使用方式是继承,子类经过继承同步器并实现它的抽象方法来管理同步状态。高并发
AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操做,固然AQS能够确保对state的操做是安全的。性能
AQS经过内置的FIFO同步队列来完成资源获取线程的排队工做,若是当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构形成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。优化
CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程若是获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构形成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。ui
在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),其定义以下:this
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; Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { } Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { this.waitStatus = waitStatus; this.thread = thread; } }
CLH同步队列结构图以下:
学了数据结构的咱们,CLH队列入列是再简单不过了,无非就是tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。代码咱们能够看看addWaiter(Node node)方法:
private Node addWaiter(Node mode) { //新建Node Node node = new Node(Thread.currentThread(), mode); //快速尝试添加尾节点 Node pred = tail; if (pred != null) { node.prev = pred; //CAS设置尾节点 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //屡次尝试 enq(node); return node; }
addWaiter(Node node)先经过快速尝试设置尾节点,若是失败,则调用enq(Node node)方法设置尾节点
private Node enq(final Node node) { //屡次尝试,直到成功为止 for (;;) { Node t = tail; //tail不存在,设置为首节点 if (t == null) { if (compareAndSetHead(new Node())) tail = head; } else { //设置为尾节点 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
在上面代码中,两个方法都是经过一个CAS方法compareAndSetTail(Node expect, Node update)来设置尾节点,该方法能够确保节点是线程安全添加的。在enq(Node node)方法中,AQS经过“死循环”的方式来保证节点能够正确添加,只有成功添加后,当前线程才会从该方法返回,不然会一直执行下去。
过程图以下:
CLH同步队列遵循FIFO,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),然后继节点将会在获取同步状态成功时将本身设置为首节点,这个过程很是简单,head执行该节点并断开原首节点的next和当前节点的prev便可,注意在这个过程是不须要使用CAS来保证的,由于只有一个线程可以成功获取到同步状态。过程图以下: