在ReentrantLock
中,线程有两种加锁的方法。线程的行为取决于它使用哪种方法来尝试获取锁。
-
使用
tryLock()
方法:
当线程调用tryLock()
方法时,它会尝试立即获取锁。如果锁当前可用(即没有被其他线程持有),那么线程会成功获取锁并继续执行。如果锁不可用,tryLock()
会立即返回false
,线程不会进入任何队列,而是会继续执行后续的代码。 -
使用
lock()
方法或不带参数的acquire()
方法:
当线程调用lock()
方法或不带参数的acquire()
方法时,如果锁不可用,线程会进入ReentrantLock
内部维护的CLH队列中等待。CLH队列是基于链表结构的等待队列,用于存放那些未能立即获取到锁的线程。进入队列后,线程会进行自旋等待,不断检查锁的状态,直到锁变得可用或者线程被中断。
在CLH队列中,线程不会立即阻塞,而是会进行自旋尝试获取锁。自旋是一种避免线程阻塞的技术,它让线程在一段时间内持续尝试获取锁,而不是立即进入阻塞状态。这可以减少线程上下文切换的开销,提高并发性能。然而,如果自旋时间过长而锁仍未变得可用,线程最终可能会选择进入阻塞状态,等待其他线程释放锁。
需要注意的是,ReentrantLock
默认的公平锁策略是非公平的,即等待时间最长的线程并不一定会优先获得锁。线程在CLH队列中的顺序可能会受到多种因素的影响,包括线程调度和锁的竞争程度等。
ReentrantLock
没有获取到锁的线程何时返回或进入CLH队列取决于它使用的获取锁的方法。使用tryLock()
方法会立即返回,而使用lock()
方法或不带参数的acquire()
方法则会使线程进入CLH队列等待。
下图是使用lock进行加锁的流程图:
给大家介绍一下trylock()方法
ReentrantLock
的tryLock()
方法实现是Java并发包中的一部分,它的具体实现可能会根据Java版本的不同而有所变化。以下是一个简化的tryLock()
方法的实现示例,它展示了ReentrantLock
如何尝试获取锁的基本逻辑:
public final boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
这里调用了sync
对象的nonfairTryAcquire(int acquires)
方法。sync
是ReentrantLock
内部的一个同步器(Sync
),它继承了AbstractQueuedSynchronizer
(AQS)。nonfairTryAcquire
方法尝试以非公平的方式获取锁。
非公平尝试获取锁的逻辑大致如下:
- 检查当前线程是否已经是锁的持有者。如果是,则增加重入次数并返回
true
。 - 否则,检查锁是否当前未被任何线程持有(即
state
字段是否为0)。 - 如果锁未被持有,则尝试使用CAS(Compare-and-Swap)操作将锁状态从0设置为1,从而获取锁。如果CAS操作成功,则返回
true
。 - 如果CAS操作失败,说明锁已经被其他线程持有,此时直接返回
false
。
完整的nonfairTryAcquire
方法实现可能包括更多的细节和边界情况处理,但上述步骤概括了其核心逻辑。
注意:由于Java代码库的实现可能随时更新,这里提供的代码仅用于解释概念,并不代表ReentrantLock
的tryLock()
方法在任何特定Java版本中的确切实现。要查看具体版本的实现,你需要查阅对应版本的Java源代码。
另外,ReentrantLock
还有一个带超时的tryLock(long time, TimeUnit unit)
方法,它的实现会更加复杂,因为它需要处理超时逻辑和可能的线程中断。这个方法会尝试在给定的超时时间内获取锁,如果超时时间到达仍未获取到锁,或者线程在等待过程中被中断,那么它会返回false
。
如果使用trylock()方法,线程没有获取到锁会返回,那线程又回返回到哪里去呢?
当ReentrantLock
的tryLock()
方法没有获取到锁时,线程并不会被放入任何阻塞队列或条件队列中。相反,线程会立即返回并继续执行后续的代码。这意味着线程保持了执行的控制权,而不是被挂起等待锁变得可用。
具体来说,如果tryLock()
返回false
,那么调用它的线程会立即得知锁当前不可用,并可以根据需要执行其他任务或采取其他策略。线程可以继续执行其他不依赖于该锁的代码路径,或者可能选择稍后重试获取锁,或者在等待一段时间后再次尝试。
重要的是要理解,tryLock()
方法的非阻塞性质意味着线程不会因为未能立即获取锁而被阻塞或暂停执行。这提供了更大的灵活性和响应性,但也需要开发者自行处理未获取到锁的情况,比如通过循环重试或采取其他并发控制机制。
因此,当ReentrantLock
没有获取到锁时,线程返回到它调用tryLock()
之后的代码行,继续执行后续的逻辑。
那么问题又来了,为什么要让没有获取到ReentrantLock锁的线程返回呢?
ReentrantLock
没有获取到锁时可以返回,这主要归功于其提供的非阻塞式锁获取机制。与synchronized
的阻塞式锁获取不同,ReentrantLock
提供了多种尝试获取锁的方法,这些方法在未能立即获取锁时不会让线程进入阻塞状态。
具体来说,ReentrantLock
提供了tryLock()
方法,它尝试获取锁,如果锁立即可用并且当前线程未被中断,那么获取该锁并返回true
,否则返回false
。这种非阻塞式的尝试获取锁的方式使得线程在未能获取到锁时可以立即进行其他操作,而不是被阻塞等待。
此外,ReentrantLock
还提供了带有超时参数的tryLock(long timeout, TimeUnit unit)
方法,该方法尝试在给定的超时时间内获取锁。如果超时时间内成功获取了锁,则返回true
;如果超时时间已过而仍未获取到锁,则返回false
。这种方式允许线程在等待一段时间后放弃获取锁,从而避免长时间的等待。
这种非阻塞式的锁获取机制带来了以下好处:
-
避免死锁:线程在未能立即获取锁时可以选择放弃或执行其他任务,从而避免了因长时间等待而导致的死锁情况。
-
提高响应性:线程在等待锁的过程中可以执行其他任务或响应中断请求,从而提高了程序的响应性和灵活性。
-
更好的性能:对于某些高并发场景,非阻塞式的锁获取机制可以减少线程上下文切换的开销,从而提高程序的性能。
因此,ReentrantLock
没有获取到锁时可以返回,这使得它在处理并发问题时具有更高的灵活性和可控性。然而,这也增加了编程的复杂性,需要开发者根据具体场景选择合适的锁获取策略。
讲完了加锁,来聊一聊ReentrantLock中的解锁逻辑:
ReentrantLock的解锁通常是通过调用其unlock()
方法来实现的。这个过程涉及到将锁的状态从占用状态设置为未占用状态,并唤醒可能正在等待获取该锁的线程。
具体来说,ReentrantLock的解锁过程可能涉及以下步骤:
- 检查当前线程是否持有锁。如果当前线程并不持有锁,那么
unlock()
方法会抛出异常,因为只有持有锁的线程才能解锁。 - 如果当前线程持有锁,那么将锁的状态减一(对应于重入的次数)。如果减一后的状态为0,表示当前线程已经完全释放了锁。
- 在锁完全释放后,会唤醒可能正在等待获取该锁的线程。这通常是通过AQS(AbstractQueuedSynchronizer)的队列管理机制来实现的,被唤醒的线程会尝试获取锁。
请注意,正确的使用ReentrantLock需要确保每个lock()
调用都有相应的unlock()
调用,且通常应在finally块中进行unlock()
调用,以确保在异常情况下也能正确释放锁。
此外,虽然ReentrantLock主要提供显式的加锁和解锁操作,但它也提供了tryLock()
等尝试获取锁的方法。这些方法在未能立即获取锁时不会阻塞线程,而是立即返回,因此在使用这些方法时,解锁的操作需要由程序员显式控制。
总的来说,ReentrantLock的解锁方式主要是通过调用unlock()
方法来实现的,但具体的解锁过程和步骤可能会根据具体的使用场景和代码逻辑有所不同。在使用ReentrantLock时,需要仔细考虑锁的获取和释放,以确保程序的正确性和性能。