这一部分重点解析一下公平锁和非公平锁:
背景:默认情况下,ReentrantLock采用的是非公平锁,即不排队的方式。当一个线程释放锁之后,其他线程是随机获取这把锁。synchronized不支持公平的设定,而ReentrantLock提供方法设置线程的公平性。
1.创建
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
CAS操作:Compare and Swap,比较并替换。有三个参数内存地址V,旧的预期值A,要修改的新值B。每次都是比较A是否和当前实际值相同,相同则替换,不同则替换失败。原子操作的底层用此操作实现。
多个线程调用lock()方法的时候, 如果当前state为0, 说明当前没有线程占有锁, 那么只有一个线程会通过CAS操作获得锁, 并设置此线程为独占锁线程。
如果更新失败,那么其它线程会调用acquire方法来竞争锁,后续会全部加入同步队列中自旋或挂起。有一种情况会出现不公平的现象,当有其它线程A进来想要获取锁时,恰好此前的某一线程释放锁, 那么A会抢先获取锁。而同步队列中未被取消获取锁的线程是按顺序获取锁的,因为A还没有被插入到队列中。
个人认为这样设计的目的是尽量不调整双向链表,因为不check的时候,要把当前线程加到队尾。同时,如果此时没有别的线程,还要把队首的线程再拿出来。这样调整算是一个优化的细节。
acquire方法:
public final void acquire(int arg) {
//在这里会再尝试做一次acquire,也就是上文提到的抢占
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//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;
}
}
else if (current == getExclusiveOwnerThread()) {
//判断获取锁的线程是否是当前线程
// 因为ReentrantLock是可重入锁,可以累加重入的次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
acquire分为tryAcquire和acquireQueued两个步骤。
tryAcquire调用sync的nonfairTryAcquire方法。在调用nonfairTryAcquire方法的时候,如果state为0,也就是刚好锁在lock()和acquire()中间被释放了(虽然几率很小,但也是普遍存在这种现象),再走一遍lock()中的流程;如果锁没被释放,判断获取锁的线程是否是当前线程,因为ReentrantLock是可重入锁,可以累加重入的次数,充当计数器的作用。
addWaiter方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
等待队列是双向队列实现的,并且设置了head和tail节点。生成新的Node之后,如果尾指针不为null,即队列不为空,则CAS操作添加当前节点到尾结点。如果队列为空, 或CAS设置失败, 则调用enq强制入队。
enq方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//新生成head节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//尝试把node的位置赋给tail
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
看enq方法中,当队列为空的时候,初始化头节点并且头节点和尾节点指向同一node。当队列不为空时,又做了一遍CAS操作。(所以,head是没有存线程的,tail是存了线程的?)这里个人理解是,队列不为空的时候,enq强制了一次CAS操作,防止addWaiter中的CAS操作失败。因为多线程的情况,当一个线程去插入节点到队列的时候,可能正好别的线程刚好结束一次插入队列操作。所以这个死循环会一致持续到插入完成为止。
看的出来,重入锁是一个基于CAS操作的同步控制。
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
AbstractQueuedSynchronizer在类加载的时候,执行静态代码块,取出了关键字段的内存地址放到对应的offset中,方便CAS操作。
acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
//failed 标记acquire是否成功, interrupted标记当前线程是否需要中断
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//第三次tryAcquire,如果前置节点是head,并且尝试获取锁成功了
//只有前置节点是head的时候,才能有资格去尝试获取锁。p == head代表了
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);
}
}
循环调用,获取前置结点。如果第一次循环就获取成功那么返回的interrupted是false,不需要自我中断。否则说明在获取到同步状态之前经历过挂起(返回true)那么就需要自我中断。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//判断前置节点的等待状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前置结点是SIGNAL状态,代表当前置结点是执行完成可以唤醒后续结点
return true;
if (ws > 0) {
//跳过取消的节点,找到第一个未取消的,作为node的前置节点。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果是其他状态,强行设置前置结点为SIGNAL;前置节点可能会被其他线程操作,所以用CAS。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
释放锁的过程:(公平锁的释放过程和非公平锁是一样的。因为是独占锁,只有当前拿锁的线程有释放锁的资格)
public void unlock() {
sync.release(1);
}
release的具体过程,会先调用tryRelease方法,然后当头节点不为null的时候,唤醒头节点。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
只有当前拿锁的线程有释放锁的资格,否则会抛出异常。在具体的代码中,lock和unlock次数不匹配,且unlock数大于lock次数的时候,会报这个错误。因为unlock次数等于lock次数的时候,当前线程已经释放锁了,此时exclusiveOwnerThread是null,就会出现值不相等的情况。
Q:这边没有对新的state的值做范围判断?A:很依赖独占机制,只有当前的独占锁才可以释放。源码一句废话都不写,逻辑很完整。
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;
}
unparkSuccessor方法唤醒node的后继有效节点。 为什么这边寻找节点的时候是从后往前找,我的理解是,插入队列的操作,是在enq中,是用尾插法,而且步骤是先搭尾节点的pre,再搭前置节点的next。所以当前查找如果是从前往后,有可能在某一个节点就找不到next了,但实际上后面是有尾节点的,只是刚搭了pre,还没来得及搭前置节点的next。这样从后往前查找,只要有节点存在,就会找到其pre节点,能优化“从前往后”的问题。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//当前node的状态小于0时,强制设置状态为0,表示后续节点将被唤醒
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//从后往前找,找到head后面的有效节点
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);
}
假设有三个线程t1,t2,t3分别尝试获取锁,
(1)一开始,t1获取锁,t2, t3在队列中:
每次尾插一个节点,都会把前置节点的waitStatus的值设置为-1,代表当前节点可以被唤醒。
(2)t1执行完之后,释放锁,做完释放的一系列操作后,头节点状态为0,t2被唤醒:
(3)t2开始了自旋操作,获取锁,并且成为head节点:
3.公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
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;
}
}
(1)公平锁在lock的时候,没有尝试CAS操作,这样的话,新来的线程没有插队的机会,所有来的线程必须扔到队列尾部。
(2)在tryAcquire方法中,拿同步锁的数量时,如果是0,或多一个hasQueuedPredecessors判断。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
分析一下结果为false的情况:
(1)如果是h==t ,说明此时队列为空
(2)当前线程是同步队列中的head结点的后继节点。
这也就意味着不满足这两个条件,就无法进行CAS抢占,只能走队列去获取锁。
公平锁的释放过程和非公平锁是一样的。