在前面的并发编程------CAS提到了手写lock锁,那我们现在来看看ReentrantLock的源码。
以下代码是查看的ReentrantLock(false),也就是非公平锁
ReentrantLock
ReentrantLock属于重入锁,先来看看他的无参构造方法。
可以看到默认为非公平锁,那么我们今天主讲非公平锁。
那我们要怎么才能使用公平锁?其实很简单:
这个有参构造可以看到,只要参数为true,则创建的就是公平锁,为什么主讲非公平锁?因为他的效率比公平锁高。因为我们使用的CPU资源去运行线程的,非公平和公平的差异主要在于:线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。
好了,说了这么多,直接进入正题吧。
常用参数
在了解之前,需要找到他的数据存储方式。
我们可以看到非公平锁的父类是AQS,这也是我们熟悉的通读队列器,其实数据的存放是AQS定义的一个Node对象
volatile Node prev;
volatile int waitStatus;
volatile Node next;
volatile Thread thread;
从这几个变量名我们可以知道分别是:上一个节点、等待状态、下一个节点、线程。
在涉及到上下节点,这应该也会知道这是一个双向链表,那么一旦涉及到双向链表就需要有全局的头节点和尾节点。所以我们可以找到
private transient volatile Node tail;
private transient volatile Node head;
这些参数源码看多了,自然就很熟悉了。毕竟这些大部分都是双向链表必备的一些参数。
还有一个很重要的参数:
private volatile int state;
这个参数后面在解释
重要的参数看完了。来看看锁的获取和释放。
lock()
到达这里应该就很熟悉了:
1.compareAndSetState
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
利用CAS的原子性质,来修改数据state(0表示没有线程获取到锁,1表示已经有线程获取到锁,可以为2,3,4…因为重入一次+1),达到锁的效果。
一旦锁修改成功,那么代表这个线程获取到了锁,如果没有则进行acquire(1);
2.acquire
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);
}
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;
}
可以看到最后此方法执行的是两个判断,如果当前的state状态为0,那么就允许尝试修改数据,并且将当前获取到锁的线程记录下来。
current == getExclusiveOwnerThread()
如果当前线程本来就是获取到锁的线程,那么就将state+1
如果返回false,则执行
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;
}
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;
}
}
}
}
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);
}
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
static final int SIGNAL = -1;
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 {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
addWaiter(Node mode)
建立新的节点,并且将新节点的上一个节点变为全局变量的尾节点,通过CAS将尾节点改为新节点,一旦尾节点更新成功,那么就将原来的尾节点的下一个节点设置为新节点。这里就是将新的节点链接到链表上去。
enq(final Node node)
如果尾节点为null,那么就创建新节点并设置为尾节点,至于为什么头节点没有设置,后面的文章会提到,else里面的做法也是一样的,在尾节点将新节点添加进去,因为在尾节点为null的情况下,addWaiter(Node mode)
中的if()里面的代码块并没有进行。
acquireQueued(final Node node, int arg)
利用变量p存放node的上一个节点,那么第一个if判断就没必要看了,看二个才是重点。调用shouldParkAfterFailedAcquire(Node pred, Node node)
方法查看线程的等待状态,因为waitStatus为int类型所以默认值为0,则执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
方法,以便于后面的线程进入之后,查询到前面的节点是处于ws == Node.SIGNAL
的情况下,直接执行parkAndCheckInterrupt()
方法,将线程设置为阻塞状态。
unlock()
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) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (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);
}
}
tryRelease(int releases)
释放锁,检测是否是获得锁的线程释放,否则抛出异常,如果是获得锁的线程释放,那么检测state值是否为1,不为1,则代表使用了重入性,那么只需要将state设置为c即可,如果为1,则代表此线程需要释放锁了,就将记录锁线程的变量exclusiveOwnerThread设置为null,并返回true。在每一个线程阻塞之前都执行了shouldParkAfterFailedAcquire(p, node)
方法,目的就是为了让这个阻塞线程的前一个节点的waitStatus变为-1,在后期线程激活之后,并且需要被激活的线程的上一个节点已经晋升为头结点,这时候获取头结点,他的头节点的waitStatus肯定不为0,所以一定成立。那么就直接执行unparkSuccessor(Node node)
方法,从尾节点找到第一个waitStatus 小于等于0的线程,释放此线程。
那为什么说这事一个不公平的呢?他还不是一样只激活了一个线程?
其实,并不然,如果在此线程在激活的过程中,新增了一个线程,那么这个新增的线程就可以和激活的线程争取机会,看谁获得锁,我们看看公平锁里面就知道了,他做了判断的,只能激活的这个线程才能修改state。
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());
}
这就是保证公平竞争的主要代码。