ReentrantLock
基本介绍:
除了 synchronized 关键字以外自JDK 5起,Java类库中新提供了 JUC包,其中的 java.util.concurrent.locks.Lock
接口便成了 Java 的另一种全新的互斥同步手段
。
基于Lock 接口,用户能够以非块结构(Non-Block Structured)来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步。
重入锁(ReentrantLock)是 Lock 接口最常见的一种实现,顾名思义,它与 synchronized一样是可重入的。
在基本用法上,ReentrantLock 也与 synchronized 很相似,只是代码写法稍有区别。
不过,ReentrantLock与 synchronized相比增加了三项高级功能:
- 等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择 放弃等待 ,处理别的事情,
- 公平锁:多个线程在等待同一个锁的时候必须按照申请所得顺序一次获取锁,但是非公平锁则不保证这一点,在做锁被释放的时候,任何等待锁的线程都有机会抢占到锁,
synchronized
、reentrantLock
默认都是非公平
的。 - 多个条件变量:一个 ReentrantLock 对象可以同时绑定多个 Condition对象;在synchronized中,锁对象的wait()跟它的notify() 或者notifyAll()方法配合可必实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用newCondition()方法即可。
但是还是优先使用 synchronized的原因:
1、synchronized 是在 Java 语法层面的同步,足够清晰,也足够简单。每个 Java 程序员都熟悉 synchronized,但J.U.C 中的 Lock 接口则并非如此。因此在只需要基础的同步功能时,更推荐 synchronized。
2、 Lock应该确保在 finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁
。这一点必须由程序员自己来保证,而使用synchronized 的话则可以由 Java 虚拟机来确保即使出现异常,锁也能被自动释放。
源码分析:
下面的源码分析会涉及到AQS 相关知识
- Sync:是提供AQS实现的工具,提供了抽象的lock()。
- FairSync(公平锁):线程获取锁的顺序和调用lock()的顺序一样,FIFO。
- NoFairSync(非公平锁):线程获取锁的顺序和调用lock()的顺序无关,抢到CPU的时间片即可调度。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/**
同步器提供所有实现机制
*/
//维护了一个Sync,对于锁的操作都交给sync来处理
private final Sync sync;
/**
* 返回与此Lock实例一起使用的Condition实例。
当与内置监视器锁一起使用时,返回的Condition实例支持与Object监视器方法( wait 、 notify和notifyAll )相同的用法。
如果调用任何Condition等待或信号方法时未持有此锁,则会引发IllegalMonitorStateException 。
当条件等待方法被调用时,锁被释放,并且在它们返回之前,锁被重新获取,并且锁保持计数恢复到调用该方法时的数量。
如果线程在等待时被中断,则等待将终止,将抛出InterruptedException ,并且线程的中断状态将被清除。
等待线程按照 FIFO 顺序发出信号。
从等待方法返回的线程重新获取锁的顺序与最初获取锁的线程相同,默认情况下未指定,但公平锁有利于那些等待时间最长的线程。
*/
public Condition newCondition() {
return sync.newCondition();
}
/**
* 查询当前线程持有该锁的次数。
*/
public int getHoldCount() {
return sync.getHoldCount();
}
/**
*查询当前线程是否持有该锁。
*/
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
/**
* 查询该锁是否被任何线程持有。该方法设计用于监视系统状态,而不是用于同步控制。
*/
public boolean isLocked() {
return sync.isLocked();
}
/**
* 如果此锁的公平性设置为 true,则返回true
*/
public final boolean isFair() {
return sync instanceof FairSync;
}
/**
* 返回当前拥有此锁的线程,如果不拥有则返回null 。当该方法由非所有者的线程调用时,返回值反映当前锁状态的尽力近似值。例如,即使有线程尝试获取锁但尚未这样做,所有者也可能暂时null 。此方法旨在促进提供更广泛的锁监视设施的子类的构造
*/
protected Thread getOwner() {
return sync.getOwner();
}
/**
* 查询是否有任何线程正在等待获取该锁。请注意,由于取消可能随时发生,因此true返回并不能保证任何其他线程将获取此锁。该方法主要设计用于监视系统状态。
*
* @return {@code true} if there may be other threads waiting to
* acquire the lock
*/
public final boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
/**
* 查询给定线程是否正在等待获取此锁。请注意,由于取消可能随时发生,因此true返回并不能保证该线程将获得该锁。该方法主要设计用于监视系统状态。
*
* @param thread the thread
* @return {@code true} if the given thread is queued waiting for this lock
* @throws NullPointerException if the thread is null
*/
public final boolean hasQueuedThread(Thread thread) {
return sync.isQueued(thread);
}
/**
* 返回等待获取此锁的线程数的估计值。该值只是一个估计值,因为当该方法遍历内部数据结构时,线程数可能会动态变化。该方法设计用于监视系统状态,而不是用于同步控制。
*
* @return the estimated number of threads waiting for this lock
*/
public final int getQueueLength() {
return sync.getQueueLength();
}
/**
* 返回包含可能正在等待获取此锁的线程的集合。由于在构造此结果时实际的线程集可能会动态更改,因此返回的集合只是尽力估计。返回集合的元素没有特定的顺序。此方法旨在促进提供更广泛监控设施的子类的构建。
*
* @return the collection of threads
*/
protected Collection<Thread> getQueuedThreads() {
return sync.getQueuedThreads();
}
/**
*查询是否有任何线程正在等待与此锁关联的给定条件。请注意,由于超时和中断可能随时发生,因此true返回并不能保证将来的signal会唤醒任何线程。该方法主要设计用于监视系统状态。
*/
public boolean hasWaiters(Condition condition) {
if (condition == null)
throw new NullPointerException();
if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
throw new IllegalArgumentException("not owner");
return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
}
/**
* 返回在与此锁关联的给定条件下等待的线程数的估计值。请注意,由于超时和中断随时可能发生,因此该估计值仅作为实际服务员数量的上限。该方法设计用于监视系统状态,而不是用于同步控制。
*/
public int getWaitQueueLength(Condition condition) {
if (condition == null)
throw new NullPointerException();
if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
throw new IllegalArgumentException("not owner");
return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition);
}
/**
* 返回一个集合,其中包含可能正在等待与此锁关联的给定条件的线程。由于在构造此结果时实际的线程集可能会动态更改,因此返回的集合只是尽力估计。返回集合的元素没有特定的顺序。该方法旨在促进子类的构建,从而提供更广泛的状态监测设施。
*/
protected Collection<Thread> getWaitingThreads(Condition condition) {
if (condition == null)
throw new NullPointerException();
if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
throw new IllegalArgumentException("not owner");
return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition);
}
}
构造器
/**
* 创建ReentrantLock的实例。这相当于使用ReentrantLock(false)
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 使用给定的公平策略创建ReentrantLock的实例。
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
获取锁资源
都是交给
sync
执行的
lock
/**
获取锁。
如果锁未被其他线程持有,则获取该锁并立即返回,并将锁持有计数设置为 1。
如果当前线程已经持有锁,则持有计数 + 1,并且该方法立即返回。
如果锁被另一个线程持有,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到获取锁,此时锁持有计数设置为 1。
*/
// 请求锁的资源,会阻塞且不处理 中断请求 没有调用 unlock 就会一直被阻塞
public void lock() {
sync.lock();
}
lockInterruptibly
/*
除非当前线程被中断,否则获取锁。
如果锁未被其他线程持有,则获取该锁并立即返回,并将锁持有计数设置为 1。
如果当前线程已经持有该锁,则持有计数将增加 1 并且该方法立即返回。
如果锁被另一个线程持有,则当前线程将出于线程调度目的而被禁用,并处于休眠状态,直到发生以下两种情况之一:
锁被当前线程获取;
或者其他一些线程会中断当前线程。
如果当前线程获取了锁,则锁保持计数设置为 1。
如果当前线程:
在进入此方法时设置其中断状态;
或者获取锁时被中断,然后抛出InterruptedException并清除当前线程的中断状态。
在此实现中,由于该方法是显式中断点,因此优先响应中断而不是正常或可重入的锁获取。
*/
//线程在请求lock并被阻塞时,如果被interrupt,则此线程会被唤醒并被要求处理;就是说: 获取锁时响应中断
public void lockInterruptibly() throws InterruptedException {
// 当尝试获取锁失败后,就阻塞可中断的获取锁的过程。调用AQS.doAcquireInterruptibly()
sync.acquireInterruptibly(1);
}
tryLock
/**
仅当调用时锁未被其他线程持有时才获取锁。
如果锁未被其他线程持有,则获取该锁,并立即返回true值,并将锁持有计数设置为 1。
即使此锁已设置为使用公平排序策略,对tryLock()调用也将立即获取该锁(如果该锁可用),无论其他线程当前是否正在等待该锁。
这种“闯入”行为在某些情况下可能有用,尽管它破坏了公平性。如果您想遵守此锁的公平性设置,请使用几乎等效的tryLock(0, TimeUnit.SECONDS) (它还检测中断)。
如果当前线程已持有此锁,则持有计数将增加 1 并且该方法返回true 。
如果锁被另一个线程持有,则此方法将立即返回false值
*/
// 尝试获取锁,默认获取的是非公平锁,失败后不会阻塞 直接返回true或false
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
/**
如果在给定的等待时间内锁未被其他线程持有并且当前线程未被中断,则获取锁。
如果锁未被其他线程持有,则获取该锁,并立即返回true值,并将锁持有计数设置为 1。
如果此锁已设置为使用公平排序策略,则如果任何其他线程正在等待该锁,则不会获取可用锁。这与tryLock()方法相反。如果您想要一个允许闯入公平锁的定时tryLock ,请将定时和非定时形式组合在一起:
if (lock.tryLock() || lock.tryLock(timeout, unit)) { ... }
如果当前线程已持有此锁,则持有计数将增加 1 并且该方法返回true 。
如果锁被另一个线程持有,则当前线程将出于线程调度目的而被禁用,并处于休眠状态,直到发生以下三种情况之一:
锁被当前线程获取;
或者其他线程中断当前线程;
或者指定的等待时间已过
如果获取了锁,则返回true值,并将锁保持计数设置为 1。
如果当前线程:
在进入此方法时设置其中断状态;
或者获取锁时被中断,然后抛出InterruptedException并清除当前线程的中断状态。
如果指定的等待时间已过,则返回值false
如果时间小于或等于零,则该方法根本不会等待。
在此实现中,由于该方法是显式中断点,因此优先响应中断而不是正常或可重入获取锁,并且优先于报告等待时间的流逝。
*/
// 在规定时间内获取锁,获取不到则返回false
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
// 先判断是否中断,然后尝试获取资源,否则进入AQS.doAcquireNanos()
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
释放锁资源
不管是公平还是非公平锁,都会调用
AQS.release(1)
,给当前线程持有锁的数量-1
。
unlock
/**
尝试释放此锁
如果当前线程是该锁的持有者,则持有计数会递减。
如果现在保持计数为零,则释放锁。如果当前线程不是该锁的持有者,则抛出IllegalMonitorStateException 。
*/
public void unlock() {
sync.release(1);
}
内部类
Sync
/**
* 该锁的同步控制基础。下面分为公平和非公平版本。使用 AQS 状态来表示锁定的保持次数。
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* 执行Lock.lock 。子类化的主要原因是允许非公平版本的快速路径。
*/
abstract void lock();
/**
* 执行非公平的 tryLock。 tryAcquire在子类中实现,但是trylock方法都需要非公平尝试
*/
// 尝试获取锁,并返回是否成功获取锁。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取锁的状态
if (c == 0) {// 锁当前没有被持有,处于空闲状态
if (compareAndSetState(0, acquires)) { // cas 修改state
setExclusiveOwnerThread(current); //cas 成功,将锁的持有者设置为 当前线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果锁被占用,则判断占有者是不是自己,是的话,则可重入
int nextc = c + acquires; // 锁的状态加上acquires
if (nextc < 0) // overflow // 如果加上acquires后的值小于0,表示发生了溢出,抛出异常
throw new Error("Maximum lock count exceeded"); // 设置新的锁状态,并返回true表示成功获取锁。
setState(nextc);
return true;
}
return false; //如果以上条件都不满足,表示当前线程无法获取锁,直接返回false表示获取锁失败。
}
// 尝试释放锁,彻底释放后返回true。
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 计算当前状态减去释放的数量,得到变量 c
if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程是否是拥有独占锁的线程,不是就抛异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //锁空闲,没有线程持有
free = true;
setExclusiveOwnerThread(null); // 将独占锁的线程拥有者设置为 null,即锁资源已经完全释放
}
setState(c); // 状态设置为变量 c
return free; // 是否彻底释放了锁
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
NonfairSync
/**
* 非公平锁的同步对象
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
执行锁定。尝试立即插入,在失败时备份到正常获取。
*/
/*
lock():获取锁时调用AQS的CAS方法,是阻塞的。
acquire() 大致的实现过程:
先会尝试获取资源,如果获取失败,将存储线程的`节点`插入等待队列。
插入后继续根据前置节点状态状态判断是否应该继续获取资源。
如果前置节点是头结点,继续尝试获取资源;
如果前置节点是`SIGNAL`状态,就`中断`当前线程;否则继续尝试获取资源。
直到`当前线程被阻塞或者获取到资源`,结束。
*/
// cas 的尝试 把 state 的值 改为 1
final void lock() {
if (compareAndSetState(0, 1))
// 如果获取成功,则把当前线程设置为锁的持有者;
setExclusiveOwnerThread(Thread.currentThread());
/*
AbstractOwnableSynchronizer
// 设置当前拥有独占访问权限的线程
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
*/
else
// 如果获取失败,则通过AQS.acquire()获取锁;
acquire(1);
}
// 尝试获取锁
protected final boolean tryAcquire(int acquires) {
// Sync.nofairTryAcquire()。 见上面分析 //非公平锁的特点是,当锁被释放后,任何线程都有机会获取到锁,不保证先来先得。
return nonfairTryAcquire(acquires);
}
}
FairSync
/**
* 公平锁的同步对象
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 与非公平锁的区别是,此处是不能直接通过CAS修改state,而是直接走AQS.acquire()。
final void lock() {
acquire(1);
}
/**
*tryAcquire 的公平版本;除非递归调用或没有服务员或是第一个,否则不要授予访问权限
*/
//与非公平锁类似: 只不过多判断了hasQueuedPredecessors(),判断当前节点在等待队列中是否有前驱节点,如果有,则说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败;如果当前节点没有前驱节点,才有做后面的逻辑判断的必要性。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁的状态是0,表示当前没有线程持有该锁
// hasQueuedPredecessors 判断当前节点在等待队列中是否有前驱节点,
// 如果有,则说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败;
// 如果当前节点没有前驱节点,即: 没有排队的前驱线程(没有等待获取锁的线程),当前线程的节点就是头节点 && cas 的方式 尝试获取锁
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
// 设置当前线程为独占锁的拥有者线程
setExclusiveOwnerThread(current);
//获取锁成功。
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //当前线程是否为独占锁的拥有者线程。
int nextc = c + acquires; //如果是,将锁的状态增加acquires。
if (nextc < 0) //如果锁的状态增加后小于0,抛出一个错误,表示超过了最大的锁计数。
throw new Error("Maximum lock count exceeded");
setState(nextc);
// 获取锁成功
return true;
}
//获取锁失败
return false;
}
}
AQS中的
// 判断当前节点在等待队列中是否有前驱节点,
public final boolean hasQueuedPredecessors() {
// 其正确性取决于 head 在 tail 之前初始化,并且如果当前线程位于队列中的第一个线程,则 head.next 是否准确。
Node t = tail; // 以相反的初始化顺序读取字段
Node h = head;
Node s;
// h != t 同步队列中有节点、
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
快问快答
公平锁和非公平锁
从定义角度:
- 获取锁的顺序与请求锁的时间顺序一致就是公平锁,反之则为非公平锁。
- 公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。
- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。
- 因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
从源码角度:
- 当锁资源已经被占用时,每次有请求到达,就在等待队列中排队。
- 此时如果锁资源被释放了,刚好新来一个线程
- 若是非公平锁,则会直接CAS获取锁,成功则返回,不成功则加入到等待队列自旋获取,自旋过程中当前驱是队列的头结点,并且tryAcquire成功时则获取成功。
- 若是公平锁,则当前线程必须等待,锁必须给等待队列第一个线程,如果第一个线程被阻塞了,唤醒也是需要时间的,只有唤醒了才能拿锁。
AQS中有哪些资源访问模式?区别?
- 独占模式和共享模式
- 独占模式(Exclusive mode):
- 独占模式是指同一时间只允许一个线程获取资源的模式。
- 在独占模式下,只有一个线程能够获取到同步状态(即锁),其他线程必须等待。
ReentrantLock
就是使用独占模式实现的锁。
- 共享模式(Shared mode):
- 共享模式是指多个线程可以同时获取资源的模式。
- 在共享模式下,多个线程可以同时获取到同步状态(即锁),允许并发访问。
CountDownLatch
和Semaphore
就是使用共享模式实现的同步工具。
为什么ReentrantLock.lock()方法不能被其他线程中断?
原因是它是一个不可中断的锁
获取方式+ 是一个独占锁
。当一个线程调用 lock()
方法获取锁时,如果锁已经被其他线程占用,那么该线程将会被阻塞,直到获取到锁为止。这种不可中断的行为是由于 ReentrantLock
类内部使用了同步器 AbstractQueuedSynchronizer
(AQS)来实现锁的获取和释放。AQS 是一个基于等待队列的同步工具,它提供了独占模式
(exclusive mode)的获取和释放机制。在独占模式下,如果一个线程在等待获取锁时被中断,它将继续等待,直到获取到锁或等待超时。这种设计决策是为了避免出现死锁或数据不一致的情况,因为在某些情况下,线程可能需要获取锁才能执行必要的清理操作。
但是: 如果你需要一个可中断的锁获取
方式,可以使用 ReentrantLock
类的 lockInterruptibly()
方法。这个方法允许其他线程中断正在等待锁的线程,从而提供了一种可中断的获取锁的机制。