ReentrantLock
ReentrantLock
是一种可重入锁,它指的是一个线程能够对资源重复加锁。ReentrantLock
与 synchronized
类似,能够保证解决线程安全问题,但是却提供了比 synchronized
更强大、灵活的机制,例如可中断式的获取锁、可定时的获取锁等。
另外,ReentrantLock
也提供了公平锁与非公平锁的选择,它们之间的区别主要就是看对锁的获取与获取锁的请求的顺序是否是一致的,选择公平锁时,等待时间最长的线程会最优先获取到锁,但是公平锁获取的效率通常比非公平锁要低。可以在构造方法中通过传参的方式来具体指定选择公平或非公平。
公平锁
在 ReentrantLock
中,有一个抽象内部类 Sync
,它继承自 AQS
,ReentrantLock
的大部分功能都委托给 Sync
进行实现,其内部定义了 lock()
抽象方法,默认实现了 nonfairTryAcquire()
方法,它是非公平锁的默认实现。
Sync
有两个子类:公平锁 FairSync
和 NonFairSync
,实现了 Sync
中的 lock()
方法和 AQS
中的 tryAcquire()
方法。
NonFairSync
NonFairSync
中 lock()
方法的实现如下:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先,非公平锁可以立即尝试获取锁,如果失败的话,会调用 AQS
中的 acquire
方法,其中 acquire
方法又会调用由自定义组件实现的 tryAcquire
方法:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
nonfairTryAcquire()
方法在 Sync
中已经默认实现:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 使用 CAS 设置同步状态
if (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;
}
这里,首先会判断的当前线程的状态是否为 0
,也就是该锁是否处于空闲状态,如果是的话则尝试获取锁,设置成功将当前线程设置为持有锁的线程。
否则的话,就判断当前线程是否为持有锁的线程,如果是的话,则增加同步状态值,获取到锁,这里也就验证了锁的可重入,再获取了锁之后,可以继续获取锁,只需增加同步状态值即可。
FairSync
FairSync
中 lock()
方法的实现如下:
final void lock() {
acquire(1);
}
公平锁只能调用 AQS
的 acquire()
方法,再去调用由自定义组件实现的 tryAcquire()
方法:
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;
}
这里唯一与非公平锁不同的是在获取同步状态时,会调用 hasQueuedPredecessors
方法,这个方法用来判断同步队列中是否有前驱节点。也就是当前线程前面再没有其他线程时,它才可以尝试获取锁。
释放锁
ReentrantLock
的 unlock
方法内部调用 AQS
的 release
方法释放锁,而其中又调用了自定义组件实现的 tryRelease
方法:
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;
}
首先,判断当前线程是否是持有锁的线程,如果不是会抛出异常。如果是的话,再减去同步状态值,判断同步状态是否为 0
,即锁被完全释放,其他线程可以获取同步状态了。
如果没有完全释放,则仅使用 setState
方法设置同步状态值。
指定公平性
在 ReentrantLock
的构造函数中可以指定公平性:
- 默认创建一个非公平的锁
public ReentrantLock() {
sync = new NonfairSync();
}
- 创建一个指定公平性的锁。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
synchronized 和 ReentrantLock 区别
这里总结一下 synchronized 和 ReentrantLock 的异同,它们之间的相同点如下:
- 都可以用于实现线程间的同步访问;
- 两者都是可重入锁,即一个线程能够对资源重复加锁;
其不同点如下:
- 同步实现机制不同:
synchronized
通过Java
对象关联的Monitor
监视器实现(不考虑偏向锁、轻量级锁);ReentrantLock
通过CAS
、AQS
和LockSupport
等共同实现;
- 可见性实现机制不同:
synchronized
依赖JVM
内存模型保证包含共享变量的多线程内存可见性。ReentrantLock
通过ASQ
中volatile
类型的state
同步状态值保证包含共享变量的多线程内存可见性。
- 使用方式不同:
synchronized
可以用于修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、同步代码块(指定的锁对象)。ReentrantLock
需要显式地调用lock
方法,并在finally
块中释放。
- 功能丰富程度不同:
synchronized
只提供最简单的加锁。ReentrantLock
提供定时获取锁、可中断获取锁、Condition
(提供await
、signal
等方法)等特性。
- 锁类型不同:
synchronized
只支持非公平锁。ReentrantLock
提供公平锁和非公平锁实现。但非公平锁相比于公平锁效率较高。
在 synchronized
优化以前,它比较重量级,其性能比 ReentrantLock
要差很多,但是自从 synchronized
引入了偏向锁、轻量级锁(自旋锁)、锁消除、锁粗化等技术后,两者的性能就相差不多了。
一般来说,仅当需要使用 ReentrantLock
提供的其他特性时,例如:可中断的、可定时的、可轮询的、公平地获取锁等,才考虑使用 ReentrantLock
。否则应该使用 synchronized
,简单方便。