锁是用来控制多个线程访问共享资源的方式,对共享资源加锁能够有效解决对资源的并发问题,比如在方法中或方法块中加synchronized关键字。在JDK5以后并发包中增加了Lock接口,用来实现锁功能。Lock提供了与Synchronized类似的同步功能,但是在使用时需要显示的获取和释放锁,故而Lock又称为“显示锁”,Synchronized则称“隐式锁”。Lock相对于Synchronized,增加了锁的可操作性、可中断性以及超时机制等等特性。
介绍Lock之前,先了解一个非常重要的组件–AQS。
AQS
AQS(AbstractQueuedSynchronizer,抽象队列同步器),是用来构建锁或者其他同步组件(如ReentrantLock、ReentrantReadWriteLock、CountDownLatch等,看下面AQS的继承关系图)的基础框架,使用了一个整型的成员变量表示同步状态,并通过内置的FIFO队列来完成资源获取线程的排队工作。
AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。
AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
AQS的设计是基于模板方法模式的,使用者需要继承AQS并重写指定的方法,然后将AQS组合在同步组件的实现中,并调用AQS提供的模板方法。
AQS主要提供了如下一些方法:
- getState():返回同步状态的当前值;
- setState(int newState):设置当前同步状态;
- compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
- tryAcquire(int arg):
独占式
获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态 - tryRelease(int arg):
独占式
释放同步状态; - tryAcquireShared(int arg):
共享式
获取同步状态,返回值大于等于0则表示获取成功,否则获取失败; - tryReleaseShared(int arg):
共享式
释放同步状态; - isHeldExclusively():当前同步器是否在
独占式
模式下被线程占用,一般该方法表示是否被当前线程所独占; - acquire(int arg):
独占式
获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法; - acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
- tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
- acquireShared(int arg):
共享式
获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式
的主要区别是在同一时刻可以有多个线程获取到同步状态; - acquireSharedInterruptibly(int arg):
共享式
获取同步状态,响应中断; - tryAcquireSharedNanos(int arg, long nanosTimeout):
共享式
获取同步状态,增加超时限制; - release(int arg):
独占式
释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒; - releaseShared(int arg):
共享式
释放同步状态;
AQS的模板方法基本可以分为三类:独占式获取与释放同步状态
、共享式获取与释放同步状态
、查询同步队列中的等待线程的情况
。顾名思义,独占式可以实现独占锁,即在同一时刻只能有一个线程获取到锁,其他需要锁的线程只能在同步队列中等待。共享式可实现共享锁,即同一时刻允许有多个线程获取到锁。
AQS的实现原理
同步队列
AQS是如何完成线程同步的?同步队列是关键。
同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒(公平锁),使其再次尝试获取同步状态。
同步队列中的节点(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;
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;
}
}
节点是构造同步队列的基础,同步队列的基本结构如下图:
AQS包含了两个节点的引用,一个指向头节点(head),一个指向尾节点(tail)。当一个线程成功获取同步状态时,其他线程无法获取到同步状态,转而成为节点加入同步队列的队尾,而这个加入队尾的过程必须要保证线程安全,为此AQS提供了一个基于CAS的设置队尾的方法:compareAndSetTail(Node expect,Node update),AQS将节点加入同步队列的过程如下:
首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,会唤醒其后继节点,后继节点将会在获取同步状态成功时把自己设置为首节点,过程如下图:
同步状态的获取与释放
独占式
AQS可以调用acquire(int arg)方法来获取同步状态,但是该方法对中断操作不敏感
,也就是说线程获取同步状态失败后进入了同步队列中,后续对线程执行的中断操作,线程不会从同步队列中移出,该方法代码如下:
//独占式获取同步状态
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//尝试获取同步状态,需子类重写
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//线程节点添加至队尾
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
// compareAndSetTail方法,CAS来确保节点能够被线程安全的添加
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//将节点添加至队列中
private Node enq(final Node node) {
// 死循环+CAS 的方式来保证线程安全
for (;;) {
Node t = tail;
if (t == null) { // 如果为空,初始化节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//获取独占式下的同步状态
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 死循环+CAS 的方式来保证线程安全
for (;;) {
final Node p = node.predecessor();
// 只有前驱节点是头节点才能尝试获取同步状态
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//检查线程是否被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
从源码可看出逻辑,首先调用AQS的tryAcquire(int arg)方法,该方法是protected修饰的,需要继承的子类重写该方法,该方法的目的是保证线程安全的获取同步状态。!tryAcquire(arg) 表示如果同步状态获取失败,则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),构造独占式(Node.EXCLUSIVE)同步节点,并调用addWaiter(Node node)方法把该节点加入到同步队列的队尾,最后调用acquireQueued(final Node node, int arg)。从acquireQueued方法的源码中可以看出,该节点以死循环的方式执行tryAcquire(arg)方法尝试获取同步状态,前提条件是前驱节点是头节点,才能获取同步状态
,原因有两点:
- 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是不是头节点。
- 维护同步队列的FIFO原则。
独占式同步状态的获取流程:线程先获取同步状态,获取失败后,生成新节点,并安全地加入同步队列队尾,接着在死循环中判断前驱是否为头节点,若是,则获取同步状态,若不是或者获取同步状态失败的情况下,线程进入等待状态,继续判断前驱是否头节点(自旋),如果同步状态获取成功,则将当前节点设置为头节点
。
同步状态获取了,执行了相应的操作后,就需要释放同步状态,使得后续节点能够继续获取同步状态。调用AQS的release(int arg)方法可以释放同步状态,代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//唤醒处于等待状态的线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 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);
}
该方法在释放了同步状态后,会唤醒其后继节点,使后继节点重新尝试获取同步状态。
共享式
共享式与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
调用AQS的acquireShared(int arg)方法就可以共享式地获取同步状态,该方法代码如下:
//共享式获取同步状态
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//尝试获取共享式同步状态,需子类重写
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//共享式获取同步状态
private void doAcquireShared(int arg) {
//共享式节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
//状态值r大于等于0时表成功获取到同步状态
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
从代码中可以看出,在acquireShared方法里,AQS调用tryAcquireShared(int arg)方法尝试获取同步状态,其返回值类型是int类型的状态值,当返回值大于等于0时,表能获取到同步状态。所以,在共享式获取同步状态的自旋过程中,成功获取到同步状态并退出自旋的条件是tryAcquireShared(int arg)方法返回值大于等于0。
共享式获取也需要释放同步状态,调用releaseShared(int arg)方法释放同步状态,代码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
//共享式释放同步状态
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
//等待状态值
int ws = h.waitStatus;
//等待状态值等于等待状态
if (ws == Node.SIGNAL) {
//如果将状态值从等待态设置为初始态失败时,则continue
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒后续节点
unparkSuccessor(h);
}
// 如果状态值为初始值,且从初始态设置为传播态失败时,则continue
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 如果头节点改变了,则继续循环,否则跳出
if (h == head)
break;
}
}
该方法在释放同步状态后,会唤醒后续处于等待状态的节点。它与独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态线程安全地释放,一般是通过死循环和CAS操作保证的,因为释放同步状态的操作可能会同时来自多个线程,而独占式的释放同步状态操作只会有一个线程执行
。
至此,AQS的实现原理已经熟悉得差不多了,在随后的Lock以及并发组件(如CountDownLatch、Semaphore)学习中定会如鱼得水。