Java源码学习:ReentrantLock学习

  前面学习了AQS,趁热打铁继续学习基于AQS实现的可重入锁ReentrantLock

使用ReentrantLock的方式也很简单,通常都是以下的方式来使用:

1
2
3
4
ReentrantLock lock = new ReentrantLock();
lock.lock();
// 代码
lock.unlock();

关于lock.lock()

  实现即为调用Sync对象的lock()方法,而可重入锁的同步控制基础就是内部类Sync,而Sync就是继承自AbstractQueuedSynchronizer实现的,使用AQS的state来表示持有锁的线程的数量。Sync的子类有FairSyncNonfairSync,也就是常听说的可重入锁的公平锁版本和非公平锁版本,通过在构造方法里传入true或false来选择使用公平锁或者非公平锁。

在默认情况使用无参构造方法,创建的是非公平锁即NonfairSync

先看下非公平锁的lock()

1
2
3
4
5
6
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

  可以看到,先通过AQS中的CAS操作compareAndSetState在期望是0的情况下尝试将state状态设置为1,如果成功了,也就是说明了AQS中的锁是无线程持有的,那么就调用setExclusiveOwnerThread()将当前线程设置为独占模式下拥有锁的线程,即顶层父类AbstractOwnableSynchronizerThread exclusiveOwnerThread属性;如果CAS操作失败,说明锁被其他线程持有,那么就调用acquire(1)进行尝试获取锁,可能会将当前线程插入同步队列队尾,这里在前面学习AQS的笔记中已经解析过。

而对于公平锁FairSynclock()

1
2
3
final void lock() {
acquire(1);
}

  则是只调用acquire(1)来参与尝试获取锁。

  前文学习AQS的时候已经知道,acquire()会先调用子类实现的tryAcquire()方法来尝试获取锁,那么再看下公平与非公平版本是分别如何实现的。

非公平锁NonfairSynctryAcquire()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

// Sync.class
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;
}

  NonfairSynctryAcqiure()是直接调用父类SyncnonfairTryAcquire()方法。再看nonfairTryAcquire()方法,先获取当前线程,调用AQS的getState()获取同步状态即锁的状态,如果状态c等于0,并且通过CAS设置state成功,则将当前线程设置为独占锁的线程,这里和NonfairSync#lock()第一步还是一样的(所以非公平锁还是挺霸道的,一来就是想直接独占锁,即便第一步失败了,在第二步tryAcquire的时候还是「贼心不死」,还是想尝试再直接独占锁);如果同步状态c不为0,再判断当前线程是否是独占锁的线程,如果是,则对状态c进行累加,即标明重入次数,将新的状态值进行设置。在上面两步判断中,如果CAS设置失败则返回false,以及如果当前线程不是锁的独占持有的线程也返回false,都说明尝试获取锁失败,在前文学习AQS已经知道,tryAcquire()失败则将当前线程插入同步队列进行等待排队获取锁。

再看公平锁FairSynctryAcquire()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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;
}

  同样也是先获取当前线程以及获取锁的状态,如果锁的状态c等于0,即没有线程持有锁,再调用hasQueuedPredecessors()判断当前线程之前是否存在排队线程,如果不存在,则通过CAS设置state,如果设置成功,则将当前线程设置为独占锁的线程,返回true;如果锁的状态不为0,并且当前线程就是独占锁的线程,那么和NonfairSynctryAcqiure()做法相同,不做赘述。

关于lock.unlock()

  实现即调用sync.release(1),由于Sync并没重写release()方法,所以无论公平锁还是非公平锁都是调用的AQS的release()

1
2
3
4
5
6
7
8
9
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

AQS再调用子类Sync实现的tryRelease()

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

  先将锁的状态减去释放的数量,这里是减1,再判断当前线程是否是独占锁的线程,如果不是,就抛出异常;再判断减1之后的锁状态值是否等于0,如果等于0,说明完全释放了锁,则将独占锁的线程设为空;继续再设置锁状态,返回释放成功与否,只有锁状态为0即完全释放了锁时,才会返回true。

  再回到release()中,tryRelease()释放成功之后,调用unparkSuccessor()唤醒后继等待的线程,前文学习AQS已学习了,不再赘述。

  通过对比,可以发现,公平锁获取锁还需要先判断同步队列中是否存在排队的线程,按照等待顺序来获取锁,而非公平锁则没这个限制,是更「霸道」的抢占,而ReentrantLock默认也是非公平锁,或许是因为非公平锁这种特征会效率更高。

其他方法

tryLock()

1
2
3
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}

  对于不带等待时间参数的tryLock(),其实公平锁和非公平锁都是调用非公平的nonfairTryAcquire(),而带有等待时间参数的tryLock():

1
2
3
4
5
6
7
8
9
10
11
12
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// AQS 中的方法:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}

  可以看到这种带有等待时间的,先调用AQS的tryAcquireNanos(),在其中,再根据公平锁还是非公平锁调用相应的tryAcquire()实现,如果tryAcquire()尝试获取锁失败,则调用doAcquireNanos()以独占定时的模式获取锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  其实对比acquireQueued()方法,可以发现,相差不大,只是多了对时间的判断,在doAcquireNanos()中,先计算出deadline,也就是设置的定时的结束时间,然后就是和acquireQueued()类似的将当前线程加入等待队列,后面在死循环中遍历前驱节点,直到前驱节点是头节点并且尝试获取锁成功,在循环过程中加了对时间的判断,如果超过了设置的定时结束时间,则直接返回false退出。

秋月 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
随缘打赏,您的支持将鼓励我继续创作!