本篇只介绍一下ReentrantLock平时最常使用的相关方法,后续再结合AQS中的Condition再写一篇。
1 ReentrantLock结构
用于线程同步的机制,它们允许多个线程同时访问共享资源,并确保线程安全。与 synchronized 块相比,ReentrantLock提供了更多的灵活性和控制权。为了更好的理解ReentrantLock,建议先了解AbstractQueuedSynchronizer。ReentrantLock逻辑相对比较简单,复杂的逻辑在AQS中。建议先看AQS。
Sync有两个实现,分别是我们最常说的公平锁FairSync和非公平锁NonfairSync。
2 ReentrantLock构造方法
无参构造方法,默认创建非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
有参构造,传入true创建公平锁,false创建非公平锁。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
3 Lock方法
所谓公平和非公平指的是获取锁的时候是否需要考虑按部就班的排队。
3.1 公平锁FairSync
lock方法调用的是AQS的acquire方法,然后调用tryAcquire获取锁,获取失败再入队列阻塞线程。我们看下公平锁核心获取锁的逻辑。
final void lock() {
acquire(1);
}
先看下方法得调用逻辑:
何为公平,先获取锁的线程,应该先得到锁,后来的要乖乖往后排队。我们看下源码:
- **if (c == 0)**首先判断锁是不是空闲的,如果是空闲的才能去获取锁
- **if (!hasQueuedPredecessors() && compareAndSetState(0, acquires))**对于公平锁来说,锁空闲是不够的,还要判断队列中有没有在等待获取锁的线程,如果有,那不好意思,排队的先获取,当前的往队列后排。
- **else if (current == getExclusiveOwnerThread())**锁不是空闲的情况下,再判断是不是刚好当前线程正在持有锁,如果是的话,也能成功获取到锁,这里就是经常说的锁重入呀。同一个线程lock锁了几次,就要相应的unlock几次,否则会导致死锁。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//锁是不是空闲的
if (c == 0) {
// 队列中是不是有等待的线程,如果没有且当前线程能后CAS上锁成功
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;
}
3.2 非公平锁NonfairSync
和公平锁不同的是,非公平锁lock方法首先就去尝试获取锁,不管队列中是否有等待的线程,获取不到,调用的是AQS的acquire方法,然后调用tryAcquire获取锁,获取失败再入队列阻塞线程。我们看下非公平锁核心获取锁的逻辑。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
先看下方法得调用逻辑:
非公平锁,不管三七二十一,我需要锁就去获取,不考虑是不是有其它线程在排队。相对公平锁来说,这里少了一步判断队列中是否有排队的线程。其它逻辑基本一样。
- **if (c == 0)**首先判断锁是不是空闲的,如果是空闲的才能去获取锁
- **if (compareAndSetState(0, acquires))**CAS能成功,表示锁被当前线程持有了,那就返回true,表示获取锁成功。
- **else if (current == getExclusiveOwnerThread())**锁不是空闲的情况下,再判断是不是刚好当前线程正在持有锁,如果是的话,也能成功获取到锁,这里就是经常说的锁重入呀。同一个线程lock锁了几次,就要相应的unlock几次,否则会导致死锁。当然这里的lock后unlock是要有先后顺序的。
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;
}
可能你有疑惑:为什么非公平锁获取锁的具体逻辑在Sync里面,而不是像公平锁一样在自己的实现类里?我们继续看代码。
4 tryLock方法
尝试获取锁,不管队列中是否存在等待队列。针对上面的问题,如果该方法是NoFairSync中,那么对于创建的是公平锁的sync对象来说,要想调用改逻辑就必须自己再单独实现一次,所以将该方法的逻辑放到了父类中,只需要实现一次。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
5 unlock方法
解锁,对于公平锁和非公平锁来说,解锁的逻辑是一样的,其核心逻辑是再Sync父类中实现的,当state状态是0时,说明该线程释放了锁,AQS需要去唤醒等待队列中的线程去获取锁。
看下释放锁的源码:
- **if (Thread.currentThread() != getExclusiveOwnerThread())**解锁一定是持有锁的线程自己去解锁,不存在其它线程解锁当前线程,这样就抛出错误。
- if (c == 0) 解锁后的状态是0,则说明当前线程释放了锁,此时将锁线程清除,返回成功。
- 不是0,那就将状态变更,但是当前线程依旧持有锁。可能会有疑问tryRelease对state的操作整体来看,不是一个原子操作,会不会有并发问题啊,其实不用考虑,在同一时刻只可能有一个线程持有锁,也就是说,只有一个线程能正常的tryRelease对state操作,完全是线程安全的。
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;
}