ReentrantLock
ReentrantLock是JDK内置的显示锁,相对于隐式锁synchronized,ReentrantLock提供了比synchronized更精细化的锁控制。通过情况下,我们会这样使用显示锁……
lock.lock();
try {
//do something
} finally {
lock.unlock();
}
ReentrantLock是可重入锁,意思是当线程A得到了锁后,还可以继续拿到该锁。
ReentrantLock是独占锁,同时只能有一个线程获取该锁。
ReentrantLock和AbstractQueuedSynchronizer的关系
ReentrantLock其实是通过内部类代理实现了锁的功能,内部类继承自AbstractQueuedSynchronizer,后面会简写为AQS,锁的核心实现都来自于AQS。AQS中的tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared都抛出异常,子类如果需要,实现这几个方法即可。
先来简单看下ReentrantLock的声明:
public class ReentrantLock implements Lock, java.io.Serializable {
//同步器的实现,ReentrantLock都是通过sync代理AQS的
private final Sync sync;
//字段……
//方法……
//Sync继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
//
}
//非公平锁
static final class NonfairSync extends Sync {
}
//公平锁
static final class FairSync extends Sync {
}
}
ReentrantLock实现了Lock接口,锁的功能是通过内部静态类Sync、NonfairSync、FairSync实现的。
公平锁和非公平锁
公平锁和非公平锁的区别在于获取锁操作过程的差异,后面讲到代码时还会详细介绍,这也是锁的重点,这里简单说明下。
公平锁:检查当前线程的前置结点是否是头结点,如果不是则会继续排队等待,否则获取锁。这种获取锁的方式可以保证队列的FIFO特性,先排队的线程先获取锁,公平地获取锁。但是这种方式效率不高,假如头结点的线程释放了锁,还要去唤醒后一个线程去获取锁,线程上下文切换,成本较高。下图是公平锁的获取示意图:
图1:公平锁的获取
非公平锁:获取锁的线程首先去尝试获取锁,如果没有获取成功则会去等待队列排队等待。这种方式首先去尝试获取锁。假如头结点的线程释放了锁,若此时另一个线程恰好去获取非公平锁,那么该线程可以竞争到该锁,免去了释放锁的线程去唤醒后续线程从而引起线程上下文切换。下图是非公平锁的获取示意图:
图2:非公平锁的获取
看下ReentrantLock的构造函数:
//默认构造函数创建非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//通过参数指定创建公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock默认创建的是非公平锁,这是因为非公平锁在实际使用中效率更高。另外还可以通过ReentrantLock提供的带参数的构造函数指定创建公平锁还是非公平锁。
非公平锁的lock方法
前面说到,我们一般会通过调用ReentantLock的lock方法去获取锁,先看下lock的源码:
public void lock() {
sync.lock();
}
lock方法的源码非常简单,锁的功能通过sync代理了。当我们执行如下代码时,将会创建一个非公平锁:
//创建一个非公平锁
Lock lock = new ReentrantLock();
非公平锁是通过内部类NoneFairSync实现的,ReentrantLock有3个静态内部类,分别是Sync、FairSync和NonfairSync。下图是ReentrantLock的简单类图:
图3:ReentrantLock简单类图
该图是ReentrantLock的简单类图框架,ReentrantLock的内部类Sync继承自AQS,FairSync和NonfairSync分别继承自Sync,实现了公平锁和非公平锁。
ReentrantLock的lock方法其实是调用Sync的lock方法,而Sync的lock方法是个抽象方法:
abstract void lock();
因此子类需要去实现该抽象方法,非公平锁NonfairSync的实现如下:
//非公平锁继承了Sync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
//锁的状态是0代表当前锁是空闲的
//比较锁的状态是否是0,原子的设置为1,若设置成功,设置独占线程为当前线程
//此处体现了非公平锁的特点,先尝试获取锁,获取失败再去队列排队获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//队列排队获取锁
acquire(1);
}
//稍后会讲解该代码
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
可以看到非公平锁的lock实现思路,首先直接去获取锁,若获取锁成功,将当前锁的占有者设置为当前线程。否则调用acquire方法获取锁。
这里先说明下comareAndSetState方法,类似这种CAS方法在JDK的锁框架中大量使用,JDK的锁正是通过CAS原子操作得以实现。
//该方法是AQS的方法,通过Unsafe类原子的设置字段state为update值,当且仅当state==expect的时候,将 //state设置为update,否则不设置
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
JDK的锁框架大量使用了sun.misc.Unsafe类的compareAndSwapXXX方法,该方法是一个native方法,并且是一个原子操作,不可中断,通过该方法保证了锁操作的正确性。
CAS 操作包含四个参数: 当前对象(this)、内存偏移量(stateOffset)、预期值(expect)和新值(update)。如果内存偏移处存储的值与预期值相同,那么处理器会自动将该位置值expect更新为新值update,否则,处理器不做任何操作。不论是否更新为新值,它都会返回该位置之前的值。
直观的理解,CAS认为位置M处的值应该是V,如果执行过程中的确是这样,将位置M处的值更新为U,否则CAS什么也不做。不管什么情况下,CAS都会返回原先位置M存储的值。
回到lock方法,当if语句中CAS操作成功后,设置当前线程为该锁的独占线程。若获取失败,调用AQS的acquire方法。可以看出,这种获取锁的方式是非公平的,线程不会首先去排队获取锁,而是直接去获取锁,获取不到才会去排队等待获取锁。后面讲到公平锁还会再做比较。
AQS的acquire方法源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这段AQS的代码很重要,需要好好理解。
首先会调用tryAcquire方法,查看该方法,惊奇的发现该方法抛出了异常:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
说明AQS并没有实现该方法,由子类去实现。看下NonfairSync的tryAcquire实现:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
tryAcquire调用了父类Sync的nonfairTryAcquire方法,看下该方法的实现:
//获取非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//如果state=0,说明没有任何线程获取锁,通过CAS去获取
if (compareAndSetState(0, acquires)) {
//设置成功后设置锁的拥有者为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//到这里,说明锁已经被某个线程获取,判断是否当前线程获取的,因为ReentrantLock是可重入锁,同一个 //线程可以多次获取锁
else if (current == getExclusiveOwnerThread()) {
//将state加上acquires,这里比较好理解
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//获取锁失败返回false
return false;
}
nonfairTryAcquire的代码还是比较好理解的,还是直接看上面注释吧。。
到这里非公平锁的tryAcquire讲完了,让我们回到AQS的方法acquire,免得走得太远,找不到回去的路:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如果tryAcquire返回成功,表示当前线程获取锁成功,否则:
- 调用addWaiter方法,创建一个Node节点,该节点维护了当前线程的引用,将该节点加入AQS的等待队列中。
- 调用acquireQueued排队获取锁。
先介绍下Node,AQS将竞争锁的线程维护在一个双向链表中,这个链表就是锁的等待队列,链表的节点就是Node节点。参考图1,链表头维护的就是已经获取锁的线程A,后面的结点B、C和D是在等待队列中等待获取锁的线程。Node类是AQS的内部静态类,看下Node的源码:
static final class Node {
//表示该节点是共享节点,线程需要获取的是共享锁
static final Node SHARED = new Node();
//线程需要获取的是独占锁
static final Node EXCLUSIVE = null;
//下面几个int值是当前节点的状态选项
//当前节点的线程因为某些原因(超时、中断等)等不及获取锁,已经取消
//在这个状态下的结点状态不会再改变,线程也不会被阻塞
static final int CANCELLED = 1;
//如果当前结点的状态是-1,当本节点维护的线程释放锁后需要去唤醒后续的线程去获取锁
static final int SIGNAL = -1;
//表示本节点维护的线程处于条件队列中
//条件队列和等待队列不一样,同一个锁可以对应多个条件队列,当然也可以没有条件队列,
//一般是通过lock的newCondition方法获取条件队列的,条件队列中的线程被唤醒后会被转移到
//等待队列中
static final int CONDITION = -2;
//当节点的状态为-3时,该节点获取共享锁后需要通知后续的共享节点获取该共享锁
//ReentrantLock是独占锁,以后介绍读锁时会用到该选项
static final int PROPAGATE = -3;
//当前结点的等待状态,只可能为0、1、-1、-2、-3这5种状态
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() {
}
//mode参数指示了该节点是共享模式还是独占模式
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
addWaiter方法创建独占模式的节点,并将该节点入队,看下AQS的addwaiter方法源码:
private Node addWaiter(Node mode) {
//传入当前线程、节点模式,创建节点
Node node = new Node(Thread.currentThread(), mode);
//快速入队,如果入队失败,调用enq方法自旋入队
Node pred = tail;
if (pred != null) {
node.prev = pred;
//如果当前线程CAS设置尾结点失败,说明有其他线程已经修改了尾结点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//自旋入队
enq(node);
//返回入队的结点
return node;
}
addWaiter方法的实现思路很简单,首先通过CAS原子地设置尾结点,如果设置失败说明有其他线程已经修改了尾结点,调用enq方法自旋入队。自旋其实就是在for循环中不断的通过CAS尝试设置尾结点,如果设置成功说明入队成功,否则继续for循环。enq是AQS的方法,看下enq的源码:
private Node enq(final Node node) {
//for循环内通过CAS设置尾结点,设置成功说明入队成功,否则设置失败,继续下一个for循环
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;
}
}
}
}
注意,enq中的comareAndSetHead方法是通过比较当前头结点是否为null,如果为null才会去设置头结点,否则说明其他线程已经设置了头结点,该线程不再去设置。
至此addWaiter方法结束,再看下acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter方法将结点入队后,继续调用acquireQueued方法去等待队列排队获取锁,看下acquireQueued方法的源码:
final boolean acquireQueued(final Node node, int arg) {
//failed表示是否获取锁失败
boolean failed = true;
try {
//等待获取锁过程中线程是否被中断过
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//前置结点是头结点,尝试获取
if (p == head && tryAcquire(arg)) {
//获取成功后设置头结点为当前结点
setHead(node);
//快速的被垃圾收集器收集
p.next = null;
failed = false;
//返回是否被中断过
return interrupted;
}
//获取锁失败后是否需要挂起当前线程
//如果需要挂起当前线程,则将当前线程挂起,返回线程在挂起过程中是否被中断过
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//线程被唤醒后,继续for循环
interrupted = true;
}
} finally {
if (failed)
//获取锁失败,取消当前结点
cancelAcquire(node);
}
}
先说下acquireQueued的思路:
acquireQueued字面意思是在等待队列中获取锁,它确实也是做的这件事。首先它会尝试去获取锁,只有前置节点是头结点的情况下,并且尝试获取锁成功,才会真正获取锁。如果尝试获取锁失败,接下来它需要判断是否要将自己挂起,当前线程在挂起自己之前需要去队列中找到一个小伙伴,告诉该小伙伴当它释放锁之后需要通知自己去获取锁,这样当前线程才能安心的挂起自己了。
当前线程找到通知自己的小伙伴后,设置小伙伴节点的waitStatus为SIGNAL,这样当小伙伴节点释放锁后就知道自己要去通知后续节点线程了。该节点被唤醒后需要继续for循环获取锁。
看下AQS的shouldParkAfterFailedAcquire方法的源码:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前置节点的状态已经是SIGNAL,当该节点的线程释放锁后自然会去唤醒后续线程,
//因此可以放心的挂起
//注意:只有前置节点的状态是SIGNAL,该方法才会返回true,其他情况下该方法都会返回false
return true;
if (ws > 0) {
//节点的waitStatus状态大于0的只有CANCELLED
//前置节点已经取消了,跳过所有取消的节点,直到找到未取消的节点
//并将断开的链连接上
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//设置前置节点的状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//到这里说明当前线程不需要挂起,因为前置节点并不是SIGNAL状态
return false;
}
shouldParkAfterFailedAcquire返回值表示了当前线程是否需要挂起,只有在当前节点的前置结点状态为SIGNAL时,该方法才会返回true,当前线程可以放心地挂起,否则返回false,当前线程不能挂起。
当前置节点的状态不为SIGNAL时,该方法不是简单的返回false,而是做了一些其他工作,这些工作都是为下一次acquireQueued方法的for循环做准备的。比如本次发现前置节点不是SIGNAL时,若前置节点状态为取消状态,该方法会移除等待队列中排在当前节点前面那些所有已经取消的节点,若前置节点状态不为取消状态,该方法尝试将前置节点的状态设置成SIGNAL。这样下次for循环时,当前节点的前置状态可能就为SIGNAL的,保证能够找到最终唤醒该线程的节点。
再回顾下acquireQueued的该代码片断:
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
shouldParkAfterFailedAcquire返回true后,说明线程需要挂起,调用parkAndCheckInterrupt,该方法很简单,通过LockSupport的park方法挂起当前线程,当线程被唤醒时,返回线程是否中断过,看下AQS中的该方法:
//挂起当前线程并返回该线程挂起期间是否中断过
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//线程唤醒后返回是否中断过
return Thread.interrupted();
}
JDK锁挂起线程都是通过LockSupport来实现的,以后再详细介绍LockSupport,这里暂且不表。
继续回到acquireQueued方法,注意到方法中finally块:
finally {
if (failed)
//获取锁失败,取消当前结点
cancelAcquire(node);
}
如果获取锁失败,将当前结点取消,状态设置为CANCELLED。
到些,acquireQueued方法讲解完了,回到acquire方法,当acquireQueud返回true时,说明线程在挂起过程中被中断过,调用selfInterrupte()方法中断自己,设置线程的中断标志位。
到此为止,非公平锁的lock方法讲解完毕。调用层次有点深,做下总结:
- 非公平锁获取锁是通过ReentrantLock的NonfairSync类实现的。
- 非公平锁首先去尝试获取锁,获取不到才会去等待队列中排队获取锁。
- 非公平锁尝试获取锁失败后,将该线程包装成一个Node节点,模式为独占模式,加入等待队列末尾。
- 加入等待队列后,需要在等待队列前面找到一个节点,该节点释放锁后能够通知到刚才加入等待队列的节点,这样新加入的节点才能安心的挂起。
公平锁的lock方法
公平锁的lock方法和非公平锁的lock方法是有区别的,先看下ReentrantLock的FairSync实现:
//公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//公平锁的lock方法直接调用了AQS的acquire方法,这和非公平锁的实现区别很明显
//非公平锁先尝试去获取锁,获取失败才会去调用acquire方法
final void lock() {
acquire(1);
}
//稍后讲解公平锁的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//……
}
}
将非公平锁和公平锁的lock代码拿出来比较下:
//非公平锁lock方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//公平锁lock方法
final void lock() {
acquire(1);
}
对比下看出来两种锁获取方式的区别。非公平锁首先尝试去获取锁,获取失败才会去调用acquire方法。
两种lock方法都调用了AQS的acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这段代码之前已经解释过,这里需要注意的是AQS的tryAcquire方法是抛出异常的,需要由子类去实现。非公平锁调用的tryAcquire是由NonfairSync实现的,同样,公平锁的tryAcquire是由FairSync实现的。
先比较下非公平锁和公平锁tryAcquire的实现:
//非公平锁的tryAcquire实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//该方法是Sync类的方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//非公平锁会直接尝试获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//……
}
//公平锁的tryAcquire实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//公平锁首先判断是否有其他前置线程在等待该锁,若有则不会去获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//……
}
这两个tryAcquire方法不同之处在于公平锁在尝试获取锁之前会通过hasQueuedPredecessors方法判断是否有前置结点在等待获取锁。
锁的释放-release方法
非公平锁和公平锁的释放操作都是相同的:
public void unlock() {
sync.release(1);
}
释放锁都是调用AQS的release方法,看下AQS的release方法:
//返回锁是否空闲
public final boolean release(int arg) {
//tryRelease在AQS抛出异常,需要由子类实现
//但是tryRelease的语义在AQS已经定义了,该方法返回锁是否是空闲的
if (tryRelease(arg)) {
//释放锁成功,唤醒后续线程
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒后续线程
unparkSuccessor(h);
return true;
}
return false;
}
其中的tryRelease由子类实现,具体的,tryRelease是由静态内部类Sync实现的。看下Sync的tryRelease方法的实现:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//如果获取锁的线程不是当前线程,抛出非法监视器异常
//只有获取锁的线程才有权释放锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//c=0,说明锁已经完全释放了
free = true;
//设置锁的拥有者为null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
这段代码比较容易理解,直接看注释吧。
回到AQS的release方法,释放锁成功后,需要唤醒后续线程,AQS的unparkSuccessor方法用来唤醒后续线程:
//唤醒node的后续节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//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其实是维护了一个队列,队列的节点是AQS的内部静态类Node。这个双向链表维护了当前等待获取锁的线程,这个队列叫做等待队列。
其实AQS还可以维护另一种队列,这个队列叫做条件队列。当我们调用锁的newCondition时,其实就是创建了一个条件队列,多次调用newCondition,可以创建多个条件队列。
条件队列中的节点如果条件满足,将会被移到等待队列,等待获取锁。
锁的条件队列以后再详细说明,这里就先不说了。
参考:
- JDK 1.7.0_79源码