赞
踩
你好我的朋友,请先容许我作一个简单介绍:我是赵士杰,Java 攻城狮,CSDN 专家博主、阿里云社区“乘风者计划”专家博主,欢迎关注我的微信公众号【技术人阿杰】。
本篇文章是一篇以ReentrantLock
可重入锁为主题的技术文章,希望能够激发并且鼓舞您继续前行。
在并发编程中,多个线程可能会同时访问某个共享资源,如共享变量、共享数据结构或共享文件等。如果没有适当的同步机制,就可能出现数据不一致或数据损坏的情况。
Java 最开始使用 Synchronized 关键字提供了一种保证线程安全访问共享资源的方式。Synchronized 可以确保在同一时刻,只有一个线程可以执行被它修饰的代码块或方法。当一个线程进入一个 Synchronized 代码块或方法时,它会获取一个锁,其他需要访问该共享资源的线程需要等待该锁被释放后才能进入。这样可以避免多个线程同时修改共享资源,从而保证了数据的完整性和一致性。
在JDK 1.6以前(不包括1.6,从JDK 1.6开始,JVM 对 Synchronized 进行了优化),Synchronized 很重,也被称为重量级锁(Heavyweight Locking),注意这里的“重”是指在实现线程同步时可能带来的性能开销和复杂度。这是由于 Synchronized 的实现依赖于内核中的互斥量(mutex),即使用操作系统的资源,当线程需要获取锁时,需要进入内核态,这样会导致用户态和内核态的切换,这个切换的开销相对较大。因为涉及到了从用户态到内核态的上下文切换,需要保存和恢复线程的上下文信息,这样的操作是比较昂贵的。
从Jdk 1.5开始,Java引入了 Java.util.concurrent
包,即 JUC,它提供了 ReentrantLock
类用于替代 Synchronized
加锁,相比 Synchronized
,ReentrantLock
不仅提供了相同的互斥锁功能,还具有更多的灵活性和扩展性。
首先,我们先简单了解一下所谓的 ReentrantLock
是什么?
ReentrantLock
是 Java 中的一个可重入互斥锁,也被称为“独占锁”,属于java.util.concurrent.locks
包。它提供了一种比传统Synchronized
关键字更灵活、更强大的线程同步方法。ReentrantLock
是可重入锁,这意味着同一个线程可以多次获得同一把锁,而不会导致死锁。
ReentrantLock
特性如下:
可重入性:允许同一个线程多次获得同一把锁。
公平性选择:ReentrantLock
提供了创建公平锁和非公平锁的选择。公平锁意味着等待时间最长的线程会优先获得锁,而非公平锁则允许插队,可能会导致某些线程饥饿,但在某些场景下会有更好的性能。
条件变量:ReentrantLock
提供了与Condition
对象配合使用的能力,允许线程在特定条件下等待,或者在条件满足时被唤醒。这类似于Object
类中的wait()
和notify()
方法,但提供了更丰富的功能,如支持多个等待集。
尝试非阻塞获取锁:ReentrantLock
提供了tryLock()
方法,允许线程尝试获取锁而不会无限期地阻塞。
可中断的锁获取操作:ReentrantLock
还提供了一个可以响应中断的锁获取操作,即lockInterruptibly()
方法。这允许在等待锁的过程中,线程可以响应中断。
下面是一个使用ReentrantLock
的简单示例,演示了如何在代码中使用这个锁:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void print() {
lock.lock(); // 在进入临界区之前获取锁
try {
// 临界区代码,只有获取了锁的线程才能执行
System.out.println(Thread.currentThread().getName() + "获得了锁");
} finally {
lock.unlock(); // 确保在退出临界区时释放锁
}
}
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
Thread t1 = new Thread(demo::print);
Thread t2 = new Thread(demo::print);
t1.start();
t2.start();
}
}
要深入理解ReentrantLock
,我们需要探讨其内部的一些关键元素,这些元素共同构成了ReentrantLock
强大的同步机制。
ReentrantLock
通过实现 Lock
接口,提供了与 Lock
接口定义的锁操作一致的方法,不仅可以更灵活地控制对共享资源的访问,可以与其他 Lock
实现进行互操作,还提供了额外的功能,如可重入性、公平性等。Lock
接口定义的锁操作方法如下:
public interface Lock {
/**
* 获取锁。如果锁不可用,则当前线程将被禁用,直到获取到锁为止。
*/
void lock();
/**
* 获取锁,可响应中断。
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试获取锁
*/
boolean tryLock();
/**
* 在给定的等待时间内尝试获取锁
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 返回与该锁实例关联的条件实例
*/
Condition newCondition();
}
在 ReentrantLock
类中,声明了一个私有的(private)、不可变(final)的 Sync
类型的变量 sync
:
/** 实现同步机制的同步器 */
private final Sync sync;
Sync
是一个抽象内部类,是 ReentrantLock
内部用于实现锁机制的同步器对象。
它继承了AbstractQueuedSynchronizer
类(简称AQS)。Sync
类通过继承和扩展 AQS 来提供同步控制功能,使 ReentrantLock
支持实现公平和非公平锁的机制。AQS 是 Java 并发包中提供的一个用于构建锁和其他同步组件的框架,
Sync
类作为 ReentrantLock
的同步控制的基础,提供了一系列关键的同步操作方法:
lock()
:这是一个抽象方法,由子类实现具体的加锁逻辑。
abstract void lock();
nonfairTryAcquire(int acquires)
:这个方法尝试以非公平的方式获取锁,如果成功则返回 true
,否则返回 false
。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果锁当前没有被任何线程持有,则尝试直接获取锁。
if (compareAndSetState(0, acquires)) {
// 如果成功,则将当前线程设置为独占线程,并返回 true。
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果当前线程已经持有了该锁,则更新锁的状态并返回 true。
int nextc = c + acquires;
if (nextc < 0) // 溢出
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
在方法中会通过 getState()
获取当前锁的状态(0
表示锁可用,非0
表示锁已经被占用),再根据锁的状态执行获取锁的逻辑:
compareAndSetState(0, acquires)
原子操作尝试更新锁的状态为 acquires
。如果更新成功,意味着当前线程成功获取了锁,随后通过 setExclusiveOwnerThread(current)
设置当前线程为锁的独占者,并返回 true
。current == getExclusiveOwnerThread()
),则尝试将锁的状态增加 acquires
值(nextc = c + acquires
)。如果 nextc
小于0
,表示整型溢出,抛出 Error
异常,因为这表示锁的计数达到了最大值。否则,通过 setState(nextc)
更新锁的状态,并返回 true
。注:Sync
内部类默认实现了非公平尝试获取锁方法,而公平尝试获取锁的方法则需要子类中提供实现。
tryRelease(int releases)
:该方法用于在独占模式下释放锁操作:
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,说明锁已经完全释放,同时通过 setExclusiveOwnerThread(null)
清除对当前线程的独占。
isHeldExclusively()
:判断当前线程是否独占地持有锁。
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
getExclusiveOwnerThread()
方法会返回当前持有独占锁的线程:
true
。false
。NonfairSync
是 Sync
的一个子类,实现了非公平锁的同步机制。其特点是在尝试获取锁时,会先尝试“插队”获取锁,如果失败则按正常流程排队等待。这种方式可能导致等待时间较长的线程饥饿,但在某些场景下能提高吞吐量。
NonfairSync
类中提供了加锁和获取锁的具体实现:
lock()
加锁操作:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先尝试通过CAS操作立即获取锁,如果成功(即同步状态从0变为1),则通过 setExclusiveOwnerThread(Thread.currentThread())
设置当前线程为锁的独占者。如果直接尝试失败(表示锁已被其他线程持有),则调用 acquire(1)
方法进入正常的获取锁流程。
tryAcquire(int acquires)
获取锁操作:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
这个方法重写了 AQS 的 tryAcquire
方法,通过调用 Sync
提供的 nonfairTryAcquire(acquires)
来实现非公平的获取锁逻辑。
与 NonfairSync
类似,FairSync
是 Sync
的另一个子类,实现了公平锁的同步机制。其特点是在尝试获取锁时,会检查是否有其他线程已经在等待队列中,确保按照请求锁的顺序来获取锁,从而避免饥饿情况。
lock()
加锁操作:
final void lock() {
acquire(1);
}
tryAcquire(int acquires)
获取锁操作:
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
方法获取锁逻辑,在当前线程获取锁时,如果当前锁的状态为0(未锁定),则检查是否没有排队的前驱节点(即等待队列中没有其他线程或当前线程位于队列的首部),同时设置当前线程为独占锁的所有者,并返回 true
,表示获取锁成功。
如果锁已被当前线程持有(即 current
线程是锁的独占所有者),则支持该线程再次获取锁。
因此在公平锁的获取锁逻辑中,hasQueuedPredecessors()
方法的作用至关重要。它保证了对锁的访问是按照请求锁的顺序(即等待队列中的顺序)进行的。
ReentrantLock
提供了两个构造方法用于创建锁的实例:
/**
* 创建非公平锁实例
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* true-公平锁;false-非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock
的无参构造方法会默认创建一个非公平锁的实例;有参构造方法允许用户指定锁的公平性策略。如果 fair
为 true
,则创建一个公平锁实例,否则创建一个非公平的锁实例。
首先我们通过以下方式创建一个非公平锁的 ReentrantLock
实例:
Lock nonfair = new ReentrantLock();
Lock nonfair = new ReentrantLock(false);
然后我们调用 lock()
方法获取锁:
nonfair.lock();
由于我们的实例实际是一个非公平锁,实际上调用的是 NonfairSync.lock()
方法:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
当 CAS 操作直接获取锁失败时,会调用 AQS
的 acquire
方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在 acquire
方法中执行逻辑如下:
首先通过 tryAcquire(int arg)
方法尝试获取锁 方法尝试获取锁,如果成功返回 true
,否则返回 false
。
注:这里实际调用的是非公平锁的获取锁方法:Sync.nonfairTryAcquire(int acquires)
。
当线程获取锁失败时,调用 addWaiter(Node.EXCLUSIVE)
方法将当前线程包装成一个独占模式的节点(Node),并通过 acquireQueued(final Node node, int arg)
将当前线程(已经被包装成节点)放入队列中进行等待获取资源。具体的等待逻辑由 AQS
类提供。
加入等待队列后,线程会进行少量的自旋,尝试再次获取锁,如果仍然失败,它最终会被阻塞挂起,直到锁被释放并且该线程被选择为下一个获取锁的线程。
在释放锁时我们需要调用 unlock()
方法来释放锁:
nonfair.unlock();
在 unlock()
方法中,会调用 AQS
提供的 release
方法进行释放锁:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release
方法是 AQS 框架中处理资源释放和线程唤醒逻辑的关键方法之一。该方法用于在独占模式下释放资源,具体执行逻辑如下:
首先调用 tryRelease
方法(需要由继承AQS的同步器类来实现)释放锁。如果锁成功被释放,则返回 true
;否则返回 false
。
注:这里的锁释放逻辑实际调用的是 Sync.tryRelease(int acquires)
方法。
如果 tryRelease
返回 true
,表示资源已经成功被释放,此时会检查同步队列的头节点(head
)。如果头节点存在并且其等待状态不为0(表示可能有线程在等待唤醒),则调用 unparkSuccessor(h)
方法来唤醒它的后继节点尝试获取资源。
创建一个公平锁实例:
Lock fair = new ReentrantLock(true);
然后我们调用 lock()
方法获取锁:
fair.lock();
由于我们的实例是一个公平锁,实际上调用的是 FairSync.lock()
方法:
final void lock() {
acquire(1);
}
在 lock()
方法中,不同于非公平锁在获取锁时会先尝试直接获取锁进行“插队”,获取失败再调用 AQS
的 acquire
方法;
公平锁会直接调用 AQS
的 acquire
方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在 acquire
方法中的执行逻辑和非公平锁一样,但是在调用 tryAcquire(int arg)
方法尝试获取锁的逻辑由 FairSync
内部类具体实现,即调用 FairSync.tryAcquire(int arg)
方法:
为了实现公平机制,防止新来的线程"插队",确保等待时间最长的线程优先获得锁,在锁可用时会先通过 hasQueuedPredecessors()
方法检查等待队列中是否有比当前线程等待时间更长的线程,如果队列中有比当前线程等待时间更长的线程,意味着当前线程不应该立即获取锁;否则当前线程就会尝试获取锁。
之后的获取锁失败处理和非公平锁相同,即将当前线程进行包装并加入等待队列,直到锁被释放并且该线程被选择为下一个获取锁的线程。
参考非公平锁,非公平锁和公平锁在释放锁的逻辑是相同的。
什么是可重入锁?
可重入锁是一种支持一个线程对资源的重复加锁的同步机制。当一个线程已经持有某个锁时,它可能需要再次请求该锁。即一个线程可以多次获得同一把锁,而不会导致死锁。
可重入是如何实现的?
以非公平锁为例,当一个线程请求获取锁时,如果锁当前已经被占用,则会通过 current == getExclusiveOwnerThread()
判断锁是否被当前线程独占,如果锁已经被它自己持有,则允许该线程再次获取锁,并更新锁的状态,实现锁的可重入性。
非公平锁和公平锁的区别?
非公平锁和公平锁的实现差异主要体现在获取锁的策略上。
非公平锁允许“插队”,即如果锁当前可用(state==0
),那么当前线程可以直接获取锁,而不必等待之前已经在等待队列中的线程。
相比之下,公平锁则严格遵循先来先服务的原则,即只有当等待队列中没有线程,或者当前线程位于队列的头部时,才能尝试获取锁。
为什么 Sync
默认实现了非公平获取锁的方法,而将公平获取锁的方法留给子类实现?
非公平锁通常比公平锁具有更好的性能。因为非公平锁减少了线程在等待队列中的时间,从而减少了上下文切换的次数。对于大多数场景,非公平锁已经足够高效且公平性的需求并不是很强。
因此Sync
提供了非公平锁的默认实现,以满足大多数情况下对性能的需求;公平锁则作为一种可选的、可能会影响性能的特性,留给了需要它的子类实现。
ReentrantLock
的 state 与 AQS 的 state 有什么含义?
AbstractQueuedSynchronizer
(AQS)为构建锁和其他同步类提供了一个基础框架,而 state
字段则是这个框架中用于跟踪同步状态的核心变量。
ReentrantLock
使用 AQS 的 state
来追踪锁被重入的次数。当一个线程首次获取锁时,state
从0变为1。如果同一个线程再次获取这个锁(即重入),state
会增加(变为2、3等),每次释放锁时 state
减少,直到 state
再次为0,锁才被完全释放。
非公平锁的插队是如何实现的?
当调用 lock()
方法时,会先执行一次 CAS 操作尝试直接获取锁,如果失败,则调用非公平获取锁策略,如果锁可用则再次执行一次 CAS 操作:
即在非公平锁中,当一个线程请求锁时,即使有其他线程正在等待,它也会直接尝试获取锁。如果在此时锁可用(即没有其他线程持有锁),请求锁的线程就有可能直接获取锁,而不用进入等待队列,从而实现了插队。这种插队行为提高了锁的获取效率,但在高竞争环境下可能导致等待时间较长的线程出现饥饿现象。
公平锁的公平是如何保证的?
与非公平锁相比,公平锁在当前线程获取锁时考虑了等待队列中的其他线程,会先通过 hasQueuedPredecessors()
方法检查等待队列中是否有其他线程已经在等待获取锁,确保按照请求锁的顺序(即等待队列中的顺序)来授予锁。
这样可以防止“饥饿”现象,即某些线程可能会长时间等待而始终无法获取到锁。通过仅在当前线程是队列中的第一个(或没有其他线程在等待)时才尝试获取锁,确保了长时间等待的线程最终能够获得锁。
公平锁和非公平锁的适用场景?
公平锁适用于那些需要确保线程获取锁的顺序性的场景,可以避免线程饥饿,但性能可能较低,因为维护一个有序队列需要额外的开销。
非公平锁在大多数场景下提供更好的性能,因为它减少了线程调度的开销,但可能导致线程饥饿。
在实际应用中,推荐使用默认的非公平锁,以获得更好的性能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。