ReentrantLock的公平/非公平锁实现
最好事先了解下CLH队列锁,也要知道CAS,再了解下LockSupport(比wait,notify好用)
参考: JAVA并发编程学习笔记之CLH队列锁
一、先分析 非公平锁(NonfairSync)
看一下AbstractQueuedSynchronizer类中比较重要的一些成员变量、子类
private transient volatile Node head;
//sync队列的尾节点
private transient volatile Node tail;
//个人理解成当前的占有资源计数,为0就是没有线程获得锁
private volatile int state;
//实现队列的一个内部类
static final class Node {
/** 共享等待标志 */
static final Node SHARED = new Node();
/** 排它等待标志 */
static final Node EXCLUSIVE = null;
/** 取消标志,源码中会以>0来判断该状态 */
static final int CANCELLED = 1;
/** 当前节点的后置节点需要唤醒标志 */
static final int SIGNAL = -1;
/** 当前节点在等待队列标志 */
static final int CONDITION = -2;
/**表示当前场景下后续的acquireShared能够得以执行 */
static final int PROPAGATE = -3;
//节点状态,取值是上面那几种,值为0时表示当前节点在sync队列中,等待着获取锁。
volatile int waitStatus;
volatile Node prev;
volatile Node next;
//这个节点所属线程
volatile Thread thread;
//节点在condition队列中的后置节点
Node nextWaiter;
}
还有一个比较重要的变量,是AbstractQueuedSynchronizer的父类AbstractOwnableSynchronizer中的
//保存成功获得锁的线程的引用,保证只有获得锁的线程能操作资源
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
开始源码分析
从lock()方法入手
//交给了sync代理该方法
public void lock() {
sync.lock();
}
查看NonfairSync内部类中的lock方法
final void lock() {
if (compareAndSetState(0, 1))//尝试CAS设置资源数
//设置资源数成功,那就获得锁,保存当前线程的引用
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);//没成功那就请求获取资源
}
若失败,则看AbstractOwnableSynchronizer类的acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//这里调用了tryAcquire方法,这个方法需要继承AbstractOwnableSynchronizer的子类去具体实现,
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
去看NonfairSync怎么实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
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()) {
//同一个线程,理解为锁重入吧
int nextc = c + acquires;//资源数进行累加
if (nextc < 0) // overflow 整数溢出
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置资源数,不用CAS
return true;//成功获得锁
}
return false;//没有获得锁
}
上面nonfairTryAcquire如果返回了true,那就没什么事了,线程得以继续运行,如果false,那就要看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)了,首先看addWaiter方法
private Node addWaiter(Node mode) {
//Node.EXCLUSIVE,其实是空值null,文章开头有说
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;//拿到尾节点
if (pred != null) {//不为空可以直接插入队列
node.prev = pred;
if (compareAndSetTail(pred, node)) {
//CAS设置尾节点,成功则返回新的尾节点
pred.next = node;
return node;
}
}
//当前尾节点为空,或因为多线程的操作,没有CAS操作成功
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {//死循环
Node t = tail;//拿到尾节点
if (t == null) {//为空则先初始化一个头节点
if (compareAndSetHead(new Node()))//CAS操作
tail = head;//成功则把尾节点也指向头结点
} else {
node.prev = t;//node的前驱节点设为尾节点
if (compareAndSetTail(t, node)) {//CAS设置尾节点为node
t.next = node;//前任尾节点的后置节点设为node
return t;//返回前任尾节点
}
}
}
}
/**
说明一下,看这个代码的时候有个疑问。
【问题1】如果队列为空,那会设置一个空节点为头结点,然后再把要插入的节点作为其后置节点,而不是直接把要插入的节点作为头结点?
*/
回顾下,上面要看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法,addWaiter看完了,它返回的是新插入节点,就看acquireQueued方法,arg是请求的资源数,前面传的是1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//先立一个flag,一开始是true,表示失败
try {
boolean interrupted = false;//这个是中断标志,标志线程要获锁的过程中有没有被触发中断
for (;;) {
final Node p = node.predecessor();//获得前驱节点
if (p == head && tryAcquire(arg)) {
//如果前驱节点是头结点,则尝试获得锁(前面说了该方法)
//这就回答了前面的【问题1】,
setHead(node);//成功后设置头结点
p.next = null; // help GC
failed = false;//没有失败,设置为false
return interrupted;//中断标记
}
//看下面说明
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果上面都为true
}
} finally {
if (failed)//上面是死循环,没看到有break之类的,这里就是抛异常,failed没被改变的情况了
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//前驱节点的状态
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {//大于0是取消状态,文章开头讲了
/*前驱节点被取消,需要重新找前驱节点*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);//一直循环到前驱节点的状态小于等于0
pred.next = node;
} else {
/*设置前驱节点为SIGNAL状态 */
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//这段代码就是保证当前节点的前驱节点(不能是取消的节点),其状态为SIGNAL
回顾一下上面if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()),shouldParkAfterFailedAcquire返回true了,就看parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//个人理解是休眠线程
return Thread.interrupted();//返回线程的中断标志,同时清除中断标志
}
这里如果返回结果决定上面acquireQueued方法的返回结果,在回顾下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
所以如果parkAndCheckInterrupt返回了true,那最后selfInterrupt会重新给线程设置中断标志位true
再来看看cancelAcquire,就是取消请求资源
private void cancelAcquire(Node node) {
if (node == null)//判空
return;
node.thread = null;
// 跳过取消的节点,直到找到没取消的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//这个用于待会CAS操作
Node predNext = pred.next;
// 设置取消状态,其他线程会跳过
node.waitStatus = Node.CANCELLED;
// 如果是尾节点,直接设置pred为尾节点就行
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
//条件:不是头节点,且(signal状态或非取消状态并成功设置成signal状态),且线程引用不为空
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);//设置成pre的后置节点
} else {
//说明pred是头结点,或状态不是
unparkSuccessor(node);
}
node.next = node; // help GC这里把next置空,下次队列调整,该节点就能达到回收的条件了
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)//还没取消,设置为等待状态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; //从尾部开始找到未取消的节点(不等于node)
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒该节点的线程
}
说明: cancelAcquire就是为了把节点node从队列中删除,首先把node的waiteStatus设为取消、thread设置为空,然后一直找到该节点的未取消的前驱节点pre,如果pre不是头结点、状态还是singal,那就可以断开node的next节点,拼接到pre后面;否则就要唤醒node的后置节点,后置节点会进行队列的调整(shouldParkAfterFailedAcquire方法)
好了,lock方法分析完了,现在看unlock方法
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒后置节点,看上面
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//单前资源数减去释放的资源数
//这里保证是获得锁的线程才能进行释放操作
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//为0说明当前线程释放全部资源了(等于释放锁)
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放锁比较简单,当获得锁的线程所有资源时,唤醒后置线程,进行锁争夺,所以,调用unlock方法的话,其实是只有两个线程在争夺锁(前任获得锁的线程和其后置线程,所以不公平!!)
二、分析 公平锁(FairSync)
与非公平锁差别在于两个方法
final void lock() {
acquire(1);//直接请求资源
}
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;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
来看看hasQueuedPredecessors()方法
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//h!=t说明队列不止一个节点
//(s = h.next) == null 说明就一个头结点
//s.thread != Thread.currentThread()说明头结点的下一个节点不是单前线程的节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
说明:上面的 “! hasQueuedPredecessors()” 就是想保证当前sync队列不止一个节点且当前线程是头结点的下一个节点,这样短路的写法,就保证只能是队列头结点的下一个节点的线程能获得锁。
参考:
- JDk1.8源码
- AbstractQueuedSynchronizer的介绍和原理分析