0%

AQS的经典应用--可重入独占锁的原理剖析

ReentrantLock 是可重入的独占锁, 同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS 阻塞队列里面。

ReentrantLock 根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁。其中Sync 类直接继承自AQS , 它的子类NonfairSyncFairSync 分别实现了获取锁的非公平与公平策略。

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

state的含义

ReentrantLock 中, AQS 的state 状态值表示线程获取该锁的可重入次数, 在默认情况下, state的值为0 表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state 的值为1,如果CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2 , 这就是可重入次数。在该线程释放该锁时,会尝试使用CAS 让状态值减1 , 如果减1后状态值为0,则当前线程释放该锁。

锁的获取

ReentrantLock 的lock()委托给了sync 类,根据创建ReentrantLock 构造函数选择sync 的实现是NonfairSync 还是FairSync

1
2
3
public void lock() {
sync.lock();
}

非公平锁的获取

默认AQS 的状态值为0,所以第一个调用lock 的线程会通过CAS 设置状态值为1, CAS 成功则表示当前线程获取到了锁, 然后setExclusiveOwnerThread设置该锁持有者是当前线程。如果这时候有其他线程调用lock方法企图获取该锁, CAS 会失败,然后会调用AQS的acquire 方法。

1
2
3
4
5
6
7
8
final void lock() {
//cas设置状态值
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//调用AQS的acquire方法
acquire(1);
}

AQS 并没有提供可用的tryAcquire 方法, tryAcquire 方法需要子类自己定制化,ReentrantLock需要重写tryAcquire 方法。

1
2
3
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果当前AQS状态为0
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;
}

nonfairTryAcquire方法首先会查看当前锁的状态值是否为0 ,为0则说明当前该锁空闲,那么就尝试CAS 获取该锁,将AQS 的状态值从0 设置为1 ,并设置当前锁的持有者为当前线程然后返回 true 。如果当前状态值不为0 则说明该锁已经被某个线程持有,如果当前线程是该锁的持有者,则状态值加1 ,然后返回true 。 如果当前线程不是锁的持有者或者CAS获取锁失败则返回false,然后其会被放入AQS 阻塞队列。

需要注意的是,线程在CAS 获取锁前,并不会查看当前AQS 队列里面是否有比自己更早请求该锁的线程, 而是使用了抢夺策略,这就是不公平的体现。

公平锁的获取

公平锁和非公平锁的获取不同之处在于重写的tryAcquire函数。

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

公平锁和非公平锁的tryAcquire 代码的类似,不同之处在于,公平锁在设置CAS 获取锁前添加了hasQueuedPredecessors 方法检查当前线程是否有前驱节点,该方法是实现公平性的核心代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
//s为队列的第一个元素
Node s;

return h != t &&
// 队列里面的第一个元素不是当前线程的判断:
// h!=t&&s!=null&&s.thread!= Thread.cunentThread():
((s = h.next) == null || s.thread != Thread.currentThread());
}

如果当前线程节点有前驱节点则返回true,否则如果当前AQS 队列为空或者当前线程节点是AQS 的第一个节点则返回false。

锁的释放

如果当前线程持有该锁, 则调用该方法会让该线程对该线程持有的AQS状态值减1, 如果减去1 后当前状态值为0 ,则当前线程会释放该锁, 否则仅仅减1而己。同样.ReentrantLock 需要重写tryRelease方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected final boolean tryRelease(int releases) {
//更新后的可重入次数
int c = getState() - releases;
//如果当前线程不是锁的持有者,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//可重入次数变为0,将线程持有者设置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//更新可重入次数
setState(c);
return free;
}

可重入锁使用案例

下面使用ReentrantLock 来实现一个简单的线程安全的list。

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
31
32
33
34
35
36
37
38
39
40
41
public class ReentrantLockList<T> {
//线程不安全的list
List<T> list = new ArrayList<>();
//独占锁
final ReentrantLock lock=new ReentrantLock();
//添加元素
void add(T e){
lock.lock();
try {
list.add(e);
} catch (Exception exception) {
exception.printStackTrace();
} finally {
lock.unlock();
}
}
//删除元素
void remove(int index){
lock.lock();
try {
list.remove(index);
} catch (Exception exception) {
exception.printStackTrace();
} finally {
lock.unlock();
}
}
//获取元素
T get(int index){
lock.lock();
try {
return list.get(index);
} catch (Exception exception) {
exception.printStackTrace();
} finally {
lock.unlock();
}
return null;
}

}

如上代码通过在操作list前进行加锁保证同一时间只有一个线程可以对list数组进行修改,但是也只能有一个线程对list元素进行访问。