Fork me on GitHub

ReentrantLock那些事

前言

在说ReentrantLock之前,我们先说说并发吧。

在JDK1.5之前,并发处理常用的关键字synchronized。使用synchronized关键字,锁的获取和释放是隐式的,synchronized主要通过系统的monitorenter指令实现的。

那时候synchronized可以称为重量级锁,执行效率不是很高。

而Doug Lea编写的util.concurrent 包被纳入JSR-166标准。这里面就包含了ReentrantLock。

ReentrantLock为编写并发提供了更多选择。

使用

ReentrantLock的通常用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class X {
private final ReentrantLock lock = new ReentrantLock();

public void m() {
lock.lock();
try {
//TODO
} finally {
lock.unlock();
}
}
}

原理

ReentrantLock主要是通过AbstractQueuedSynchronizer实现的,是一个重入锁,即一个线程加锁后仍然可以获得锁,不会出现自己阻塞自己的情况。

UML图

我们看一下它们的UML图。

upload successful

可以看到ReentrantLock实现了Lock接口。

锁类型

ReentrantLock的两种锁类型,公平锁和非公平锁。

upload successful

upload successful

源码分析

我们先来看下ReentrantLock的构造方法。

1
2
3
4
5
6
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

可以看到默认无参构造方法为非公平锁实现。如果想定义公平锁实现,可以传入true来控制。

它的lock方法:

1
2
3
4
5
6
7
8
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

公平锁和非公平锁各有自己的实现方式。我们来看下他们的tryAcquire方法。

非公平锁源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
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;
}

可以看到,非公平锁首先判断AQS(AbstractQueuedSynchronizer)中的state是否为0,0表示没有线程持有该锁,当前线程就尝试获取锁。

如果不是0,那在判断是不是当前线程持有该锁,如果是,就会增加state,改变state状态。(因此ReentranLock支持重入)。

公平锁源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
@ReservedStackAccess
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
2
3
4
5
6
7
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

公平锁的tryAcquire方法,可以看到,相比非公平锁,多了hasQueuedPredecessors方法,这个方法是判断队列中是否有其他线程,如果没有,线程才会尝试获取锁,如果有,会先把锁分配给队列的线程,因此称为公平锁。

这儿可以看到,非公平锁的效率比公平锁要高。

这是tryAcquire方法,如果尝试获取锁失败了呢?

那就会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法啦。

我们先来看一下addWaiter方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Node addWaiter(Node mode) {
Node node = new Node(mode);

for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}

可以看到,这个方法会把线程添加到队列尾,同时,for(;;)循环保证添加成功,直到return出去。

添加后,调用acquireQueued方法,这个方法为挂起等待线程。

看下该方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

可以看到,如果节点为头节点,就尝试获取一次锁,如果成功,就返回。

否则判断该线程是否需要挂起,如果需要的化就调用parkAndCheckInterrupt挂起。

调用LockSupport.park方法挂起线程,直到被唤醒。

selfInterrupt方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0();
b.interrupt(this);
return;
}
}
interrupt0();
}

调用interrupt方法,中断正在执行的线程(如果不是当前线程的话)。

释放锁unlock方法:

公平锁和非公平锁释放锁的方法是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

可以看到首先会判断当前线程是否是获得锁的线程,如果是重入锁需要将state减完才算是完全释放锁。

释放后调用unparkSuccessor唤起挂起线程。

总结

  1. 非公平锁的效率是比公平锁要高的。
  2. ReentranLock支持重入,因为增加了对自身线程的处理,通过state可以控制。
  3. 解锁操作应放到finally块里,避免使用锁时出现资源无法释放的问题。



-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

SakuraTears wechat
扫一扫关注我的公众号
您的支持就是我创作的动力!
0%