非公平锁,顾名思义就是不公平的获取锁,只要有机会,就尝试抢占锁资源。
举个栗子,你在公共厕所排队上厕所,突然有一个人进来,尝试开了所有厕所的门,只要有没人的坑位,他就抢先蹲进去,如果没有坑位,则老老实实的排队。
我这里简单写了一个例子方便进行代码阅读
import java.util.concurrent.locks.ReentrantLock;
/**
* ReentrantLockDemo
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
reentrantLock.unlock();
}
}
首先进行了ReentrantLock对象的创建
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
这里 ReentrantLock对象创建接受一个布尔参数,判断是创建公平锁还是非公平锁,本篇文章是进行非公平锁的源码解析,所以在创建对象的时候没有传参数。
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(不了解的可以搜索CAS操作)进行状态数的原子更新
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
如果当前锁状态是0,则表示当前进程锁没有被占,则更新成1。同时直接将线程锁给当前线程。
注:这个状态数是用来计算进入锁的同一个线程的个数,0表示当前线程没有进入锁,1表示当前有一个线程进入锁。通过维护这个状态数就实现了重入锁的功能。只要是当前进程在持有这个锁,那么当前线程在想进入锁里面时候只要在状态数加1即可。
如果当前状态锁状态更新失败,则非公平锁会继续进行锁获取的尝试。源码如下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先会进行tryAcquire进行尝试获取,我们看看这个函数具体做什么操作
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* 为了方便观看,我把nonfairTryAcquire源码直接贴在下面
*/
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);
return true;
}
return false;
}
第一步获取当前线程,然后获取当前线程锁的状态,如果是0,则进一步通过CAS操作进行线程锁的状态更新。如果更新成功,则将当前线程获取锁返回true。如果当前线程锁的状态不是0,则进一步判断当先线程与获取锁的线程是不是同一个,如果是同一个,则将状态数加1,然后判断当前状态数有没有超过最大值,如果超过最大值,则抛出异常,否则更新状态数,返回true。如果都不符合这几个条件则返回false。
如果尝试获取失败则进行接下来的尝试 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),这里有两个方法,先看看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;
}
addWaiter接受了一个节点参数 Node.EXCLUSIVE 这个表示Node节点表示当前节点正在以独占模式等待。
创建一个新节点,将当前进程存进去。然后创建一个前节点,将尾节点复制给这前节点。如果这个节点不为空则将存储当前线程的节点的前置节点设置成之前的尾节点。把之前尾节点的下一个节点更新成存储当前线程的节点。说白了,就是进行节点的添加操作 。
如果尾节点为空 则进行enq操作
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
也就是当前尾节点为空表示当前没有排队的进程,则初始化一个新的链表,然后把当前节点加到这个链表中。
addWaiter方法返回了存储当前线程的节点。acquireQueued接收这个节点继续进行操作
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
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);
}
}
首先默认尝试排队成功,然后默认中断interrupted是失败的。接着进入一个死循环,首先获取传入节点的前节点(predecessor())。接着判断如果前节点为头结点,并且尝试获取锁资源成功则将排队的链表的头节点设置为none下一个节点设置为null方便jvm进行垃圾回收,排队失败,然后返回中断失败,表示当前节点获取锁成功。如果上一个节点不是头结点 或者尝试获取锁失败则进行下一个判断。
先看看shouldParkAfterFailedAcquire做了什么操作
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 如果前一个节点设置了状态为需要释放
* 则表示当前节点可以安全的挂起
*/
return true;
if (ws > 0) {
/*
* 如果前一个节点的状态为取消状态,则抛弃该节点,
* 进行链表更新
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 将前置节点的状态设置成SIGNAL
*
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这里涉及到几个信号在这里讲一下
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
具体意义自己看。回到刚刚的方法中如果前一个节点的状态不为-1的话接着寻找前一个状态不大于0的节点。如果前一个节点的状态大于0则表示这个节点为取消节点直接抛弃掉。直到前一个节点不大于0的时候返回false。
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
当前判断失败,并且当前为死循环,会重新进入到这个方法中。此时前置节点的状态应该不是-1就是0。 如果是-1则直接返回true。如果为0则会进入到最后一个判断里面将前置节点的状态设置成-1。最后再次进入到这个方法里面的时候就可以返回true了。parkAndCheckInterrupt就将线程挂起。
最后再执行根据failed的值进行 cancelAcquire(node) 操作。正常排队成功应该是不会进行取消尝试的操作的,如果才排队的过程中出现异常,则进行取消尝试操作的步骤。具体怎么取消的,请读者自己了解,这里就不做赘述了。
到这里ReentrantLock的非公平锁的锁操作的源码就读完了 。
锁释放的操作比上锁的操作简单多了。话不多说,直接上码就完了
public void unlock() {
sync.release(1);
}
调用了release方法,看看release方法内容
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
进行tryRelease方法判断释放是否成功。
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;
}
先将当前线程锁的状态数减一,接着判断当前线程跟占有独占锁的线程是不是同一个,如果不是同一个则抛出异常。如果是同一个,则继续判断当前状态数是不是0,如果为0,则表示当前线程全部退出锁了,则将独占锁的线程置成null返回true。如果状态数不为0,则表示还有进程在使用这个状态锁,则表示锁资源还不能释放,更新状态数,返回false。
释放成功后,获取当前排队链表的头节点,看看头结点是不是null而且头节点的状态码是不是0。如果当前判断条件不满足,表示当前链表里面没有排队的线程。直接返回true就好。如果当前条件满足,则表示当前链表里面还有其他线程需要锁资源,在进行排队。所以需要唤醒在排队的线程。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
先将传入的头节点的状态码设置成0,如果头节点状态码小于0的话。小于0就表示还有线程需要唤醒。这一步更新成0的成功与不成功并不影响接下来的判断,如果更新不成功,也会在接下来的等待线程改变状态。
接着获取传入头节点的后继节点。如果头结点的后继节点状态大于0或者为空。则会从链表的尾节点开始向前遍历。前面已经提到了 waitStatus大于0表示是取消排队线程。所以程序会从尾节点向前变量,知道找到一个状态码小于0的排队线程。如果找到了这样的一个节点,则唤醒这个节点线程。
至此,RenntrantLock的释放锁的操作就完成了。
PS:那这个关于ReentrantLock的非公平锁的源码就剖析完成。由于是第一次写源码的剖析博客,如有不正确的地方,欢迎广大同学指正。大家共同进步。谢谢。