0%

深入理解ReentrantLock

ReentrantLock基于AQS的独占模式,默认采用非公平锁,是一种可重入锁。

可以在构造方法中指定是公平锁还是非公平锁

使用案例

ArrayBlockingQueue中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final ReentrantLock lock;
private final Condition notFull;
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}

构造方法

1
2
3
4
5
6
public ReentrantLock() {
sync = new NonfairSync(); // 默认非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

非公平锁

非公平锁主要体现在获取锁时不判断是否有人在排队,直接cas操作尝试获取锁

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
static final class NonfairSync extends Sync {
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// 不判断队列中是否有线程在等待,直接cas操作尝试获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread()); // 把自己设置为占用锁的线程
else
acquire(1);
}
// acquire是父类AQS中的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
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) { // 为0表示锁没被占用
// 不判断队列中是否有线程在等待,直接cas操作尝试获取锁
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;
}

公平锁

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
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;

final void lock() {
acquire(1);
}
// acquire是父类AQS中的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 为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; // 获取失败,后面就是执行AQS的排队方法
}
}

总结

公平锁和非公平锁只有两处不同:

  1. 非公平锁在调用 lock 后,直接调用CAS方法进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到CLH队列等待唤醒(AQS中已实现)。

非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态

ReenTrantLock和Synchronized的对比

  1. 两者都是可重入锁

    可重入锁概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

  2. synchronized 依赖于 JVM , ReenTrantLock 依赖于 API

  3. ReenTrantLock提供了一种能够中断等待锁的线程的机制。通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

  4. ReenTrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。

  5. ReenTrantLock可实现选择性通知,调度线程更加灵活

    用ReentrantLock类可以绑定多个Condition实例可以实现选择性通知,而synchronized在使用notify、notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。

  6. synchronized 异常就会释放锁,而 ReenTrantLock 异常需要在 finally 里 unlock

  7. 性能已不是选择标准

    在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。

坚持原创技术分享,您的支持将鼓励我继续创作!