前言
在java中重入锁有两个,一个是synchronized关键字,是C++实现的,而另外一个就是本篇文章中的主角儿ReentrantLock。ReentrantLock是在jdk 1.5版本后引入的juc包中一个比较重要的工具类,由著名的并发大师Doug Lea编写;接下来就让我们一起来学习一下它吧。
AQS
在学习ReentrantLock之前,我们必须先学下AQS,它是一个实现锁和一些相关的同步器如(semaphores,events)的框架。它内部是一个独占线程,同步状态state,FIFO的等待队列和提供原子CAS方法的UnSafe对象组成。它的类图关系如下
内部组成图
FIFO等待队列
AQS内部的先进先出等待队列是由双向链表实现,这个链表是存在头结点的,即蓝色中的那个节点内部是没有线程的。而T1和T2节点中的线程会处于自旋获取锁的状态中,当然在自旋一定的时间后还获取不到锁,则会进入阻塞状态。
Node节点
Node节点有两种模式,一种共享模式,另外一种是独占模式。它内部比较值得的我们关注的是waitStatus这个成员变量,它一共有5中值,分别是-3,-2,-1,0,1。在本文中我们不讨论-3,我们先讨论-1,0,1这三个状态。
static final class Node {
/** 共享模式的节点 */
static final Node SHARED = new Node();
/** 独占模式的节点 */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
/** waitStatus状态值为1时,说明这个线程将被取消不执行了 */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
/** waitStatus状态值为-1时,说明这个节点有义务去唤醒它后继节点 */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
/** waitStatus状态值为-2时,说明这个线程在condition队列中阻塞等待 */
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() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
同步器
在介绍完上述的AQS后,我们看下ReentrantLock内部的同步器,它继承自AQS。而Sync这个抽象类有两种具体实现,分别是非公平同步器和公平同步器。当我们需要使用非公平锁的时候,则在ReentrantLock的构造函数中将sync赋值为非公平同步器,需要使用公平锁时,则sync赋值为公平同步器。而ReenrantLock默认是使用非公平锁的。
/** 同步器*/
private final Sync sync;
/** 构造函数*/
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
那非公平锁和公平锁有何区别呢,非公平锁模式下即意味着新的线程不需要到等待队列中阻塞也可以和等待队列中刚被唤醒的线程中竞争,即新线程可以插队。而公平锁模式下即意味着新线程必须去排队。
非公平锁
在介绍完同步器后,我们以非公平同步器为例子,去介绍一个线程从开始到阻塞过程。假设第一次有T1线程去尝试去获取锁,而它第一次就获得了锁。它就将exclusiveOwnerThread 设置为自身,即此时exclusiveOwnerThread = T1,state = 1
。
static final class NonfairSync extends Sync {
/*获取锁方法*/
final void lock() {
//第一次获得锁成功
if (compareAndSetState(0, 1))
//设置独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
//获取锁失败
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
紧接着有T2线程来获取锁,但此时state为1,获取锁失败。则会进入acquire方法尝试获得锁。首先它会进行第一次获得锁,而tryAcquire方法是由AQS提供的模板方法。而这里调用的是非公平同步器的实现。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在非公平获取锁方法中,会进行CAS交换,交换成功,则设置独占线程。
而当当前线程是自己的时候,也即是重入锁。会将state这状态进行叠加。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
/*重入锁的情况*/
else if (current == getExclusiveOwnerThread()) {
/** state状态进行叠加*/
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
/** 设置状态*/
setState(nextc);
return true;
}
return false;
}
假如T2线程第一次尝试获取锁失败,则要进入等待队列中,即进入addWaiter方法,而在队列初始化的时候,首先调用的是enq方法。
/*
这是第初始化的队列的方法,也即是添加第一个线程
*/
private Node enq(final Node node) {
//死循环:执行2次
for (;;) {
//第一次t->NULL和tail->NULL
//此时 head->NULL
//第二次 t->[__],t也指向空节点
Node t = tail;
//第一次判断成立
//第二次不成立了
if (t == null) { // Must initialize
//创建一个空节点,让head指针指向它
//head->[___]
if (compareAndSetHead(new Node()))
//head—>[__],tail->[__]
//tail和head都指向空节点
tail = head;
} else {
/*第二次就到这, head->[__]<-node.prev
^
|
tail,t
编程上面这样*/
node.prev = t;
//然后node编程尾结点
if (compareAndSetTail(t, node)) {
//再将空节点的next指向node
t.next = node;
//初始化完成
return t;
}
}
}
}
enq方法中,node会进入一个for的死循环中,直到变成尾结点。我们这里假设它的CAS操作第一次就成功,则总共需要两次CAS操作。
在第一次循环时,因为tail尾指针没有初始化,指向为NULL。则会进入第一个if语句块,此时CAS操作是将head指针指向一个不包含线程的虚结点,即头结点。然后tail指针也指向这个头结点。
而第二次循环,因为tail不为NULL了,则进入else语句块,node结点将prev指针指向虚节点。随后CAS更新tail指针,将tail指向node。
最后就是将t的下一个结点指向node,至此队列初始化完成。
我们假设在此同时,有线程T3也去竞争锁,但竞争失败,调用了addWaiter方法,而此时队列已经初始完成,它不会再调用enq方法入队,而是在addWaiter方法中入队。变成如下图的样子。具体过程看代码注释辣,不再详细画了。
//添加等待线程后,返回这个node
private Node addWaiter(Node mode) {
//创建一个节点,设置里面的线程为当前线程
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//游标pred 设置尾结点
Node pred = tail;
//即这个双端队列已经有元素了
if (pred != null) {
//
node.prev = pred;
//cas将node设置为尾结点
if (compareAndSetTail(pred, node)) {
//pred还停留在尾结点的上一个节点,所以将它的next指向node
pred.next = node;
return node;
}
}
//就加第一个元素时,会执行这个方法
enq(node);
return node;}
此时,T2和T3线程虽然已经入队,但它们并没有进行阻塞,会在acquireQueued中进行自旋,去第三次尝试获得锁。我们假设第一种情况,即T2并没有获取锁成功。则他们都会进入语句中。整段代码看下面。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
此时线程如下图。
我们来对shouldParkAfterFailedAcquire(p, node)
进行分析。在这个方法中有三种情况。
- SIGNAL:节点状态为-1
- CANCELLED:节点状态为1
- 默认为0:节点状态为0
而此时T2,T3节点状态都为0,则会进入else语句块,最后只剩T3状态变为0
此时T2和T3都会调用 parkAndCheckInterrupt()
方法进行阻塞,不再自旋。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
/** 获取前驱节点的状态*/
int ws = pred.waitStatus;
/** 如果前驱节点状态为-1,则会进入阻塞*/
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//将前驱节点的等待状态更新为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
而在 shouldParkAfterFailedAcquire
方法中还有一种情况值得我们注意的是前驱等待状态为CANCELLED,此时它做了什么呢?
do {
/*
将这个node的前驱和第一个不为取消状态的节点进行相连
pred = pred.prev;
node.prev = pred;
*/
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//第一个不为取消状态的节点和这个node节点相连
pred.next = node;
此时它做的事情是,找到第一个不为取消状态的节点,然后将这个为取消状态的前驱节点断开联系。
我们假设此时情况如下,T5,T6刚加进队列,T3和T4因为自旋获取锁中出现异常而进入finally块中调用取消方法。这里我们还要清楚一点,就是线程加入等待队列中,并不会立马进入阻塞,它们还是会在自旋获取锁的。所以T5线程的前驱节点T4线程的状态是1,即会找到第一个等待状态不为1的节点。也就是T2。
node.prev = pred = pred.prev;
然后将前驱连接至T2
跳出while循环后,会将T2节点的next指针指向T5。
在我们理解完shouldParkAfterFailedAcquire
这个方法后。来看下acquireQueued方法中做了啥事。其实也很简单,就是如果假如队列的节点的前驱节点是head节点,如果获取锁成功,则调用setHead
方法释放这个线程。
//而这个方法是,让刚阻塞的线程满足它前驱节点是头结点的情况下,还有一次释放的机会。
final boolean acquireQueued(final Node node, int arg) {
//默认是failde是true,这个标志是判断是否需要因为在自旋中出现异常而进行线程去程操作
boolean failed = true;
try {
//阻塞是false失败的
boolean interrupted = false;
//死循环,这个死循环是一直自旋,获得锁用的
for (;;) {
//拿到刚阻塞的线程的前驱节点
final Node p = node.predecessor();
//如果它的前驱节点是那个头节点和获取锁成功的情况下
if (p == head && tryAcquire(arg)) {
setHead(node);
//头节点和node取消关联
p.next = null; // help GC
//获取锁成功,就将failde设置为false
failed = false;
//返回Interrupted判断,即阻塞失败了
return interrupted;
}
//这个是获取锁失败了,他就会返回true进行阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
而setHead方法是怎样释放线程的呢?我们可以简单的看一下。
//释放这个线程,这个方法是
/** 让head指针指向node,将node里的线程释放后,这个node就成了
不包含线程的头结点,再将prev设置为null。和前面的头节点取消关联
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
在前面我们也提到了线程会因为在自旋中因为一场等原因获取锁失败,转而进入取消方法中cancelAcquire
,我们来看下这个方法,具体解释在注释里,首先我们先总体了解下这个方法
private void cancelAcquire(Node node) {
//如果节点为null,则返回
if (node == null)
return;
//将thread清除
node.thread = null;
// 下面这步表示将 node 的 pre 指向之前第一个非取消状态的结点(即跳过所有取消状态的结点),waitStatus > 0 表示当前结点状态为取消状态
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
//将当前节点设置为取消状态
node.waitStatus = Node.CANCELLED;
//如果当前要取消的节点是尾节点,则将第一个为取消状态的节点设置为尾结点
if (node == tail && compareAndSetTail(node, pred)) {
//并将第一个为费取消状态的节点设置为null
compareAndSetNext(pred, predNext, null);
} else {
//这种情况是我们要考虑一下的,因为要取消的节点在中间
//
int ws;
/**
如果该节点往前找的第一个非取消状态的节点不是头结点
且等待状态是SIGNAL
或者
*/
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//获得当前要取消节点的下一个节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//将第一个等待状态为非取消状态的节点的后驱节点换成next
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
//最后将尾结点连接自身,帮助Gc
node.next = node; // help GC
}
}
在我们整体看完这个方法后,我们可以分步看下这方法怎么做的。
首先是一直往前找,找到第一个不为取消状态的节点。将自己的前驱连接到它,然后将自己的状态设置为取消状态。
//一直往前找,找到第一个不为取消状态的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
找到以后,这个要取消的节点不是尾节点,即是中间节点,如果是中间节点,它就要将第一个不为取消状态的节点设为singal,因为只有这样状态的前驱节点才能阻塞后面队列中的节点线程。如果设置不成功,它就会唤醒后面的节点。
//
int ws;
/**
如果该节点往前找的第一个非取消状态的节点不是头结点
且等待状态是SIGNAL,或者能把它cas换成signal且这个节点没有被唤醒
为什么要这样做呢?
因为前面也知道,一个后面的节点要被阻塞,需要前面的节点状态是signal
*/
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//获得当前要取消节点的下一个节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//将第一个等待状态为非取消状态的节点的后驱节点换成next
compareAndSetNext(pred, predNext, next);
} else {
//如果做不到,就将后继节点给唤醒
unparkSuccessor(node);
}
//最后将尾结点连接自身,帮助Gc
node.next = node; // help GC
在讲完acquireQueued方法后,我们可以知道它其实在这方法里已经完成了阻塞,但在 acquire方法里为啥还要阻塞呢?是因为要防止线程中途唤醒而补的一次阻断,但这是怎么的情况?我也不是很清楚
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
在叙述完上述一大篇,我们终于理解完整个上锁过程了,接下来可以看下锁的释放过程。释放的过程比较简单,将状态减至0后,将独占线程设置为0即完成了释放。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。