ReentrantLock是如何实现公平非公平的?差异在哪里?
1 个回答
能治愈生活的良方,是保持对生活的热爱。叶凡**ReentrantLock** 是 Java 并发包中提供的一种用于实现显式锁的工具,它比 `synchronized` 更灵活,支持**公平锁**和**非公平锁**两种模式。 ### 1. **公平锁和非公平锁的概念** - **公平锁**:按照**请求锁的顺序**(FIFO)获取锁,等待时间最长的线程优先获得锁。公平锁确保每个线程公平地获取锁,不会出现“插队”现象。 - **非公平锁**:线程可以**直接抢占锁**,即使有其他线程已经在等待,当前线程仍然可以在没有竞争时立即获取锁,可能会导致某些线程长时间等待而其他线程频繁获取锁(可能出现“插队”)。 ### 2. **ReentrantLock 的公平与非公平实现** `ReentrantLock` 的公平性实现是通过**内部的队列机制**控制的。公平锁和非公平锁的核心差异在于:**获取锁时是否会检查等待队列中是否有其他线程已经在等待**。 - **公平锁**:严格按照线程进入等待队列的顺序依次获取锁,**不会跳过等待的线程**。 - **非公平锁**:不一定遵循顺序,可以让当前线程**直接尝试获取锁**,如果锁是空闲的,则立即获取,即使其他线程已经在等待。 `ReentrantLock` 的公平性是通过其构造函数来选择的: ```java // 默认是非公平锁 ReentrantLock lock = new ReentrantLock(); // 创建一个公平锁 ReentrantLock fairLock = new ReentrantLock(true); // 创建一个非公平锁 ReentrantLock unfairLock = new ReentrantLock(false); ``` 当传入 `true` 时,`ReentrantLock` 实现公平锁;传入 `false`(或者不传,默认是 `false`)时,使用非公平锁。 ### 3. **公平锁的实现** 在公平锁的情况下,每次线程尝试获取锁时,都会检查是否有其他线程在等待锁。如果有线程在等待,当前线程**不会立即获取锁**,而是进入等待队列按顺序排队。 - **获取锁的逻辑**:公平锁在每次获取锁时,会检查等待队列,如果队列不为空,且有线程在队列中排队,那么当前线程必须进入等待队列,并等待其他线程先获得锁。 公平锁的主要步骤: 1. 当线程尝试获取锁时,首先检查锁的状态。 2. 如果锁是空闲的,再检查等待队列是否有其他线程在排队。如果有其他线程排队,当前线程就会进入等待队列。 3. 当锁被释放后,等待队列中最前面的线程会被唤醒并获得锁。 #### 公平锁源码分析: 在 `ReentrantLock` 的 `FairSync`(公平锁内部类)中,`tryAcquire` 方法是关键: ```java 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; setState(nextc); return true; } return false; } ``` - `hasQueuedPredecessors()`:判断是否有其他线程在等待锁,如果有,则当前线程不能抢锁,而是等待排队。 - `compareAndSetState(0, acquires)`:通过 CAS 操作更新锁状态为“已获取”。 ### 4. **非公平锁的实现** 非公平锁在获取锁时,**不会优先考虑等待队列中的线程**,而是允许当前线程直接尝试抢占锁。如果锁是空闲的,当前线程可以立即获取锁;如果锁被占用,线程就进入等待队列排队。 - **获取锁的逻辑**:非公平锁在获取锁时,当前线程首先**尝试直接获取锁**,不管队列中是否有其他线程在等待。如果锁被占用,才会进入等待队列。 非公平锁的主要步骤: 1. 当线程尝试获取锁时,首先尝试直接抢占锁。 2. 如果锁是空闲的,当前线程可以立即获取锁。 3. 如果锁被占用,线程就进入等待队列排队。 #### 非公平锁源码分析: 在 `ReentrantLock` 的 `NonfairSync`(非公平锁内部类)中,`tryAcquire` 方法是关键: ```java final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 不检查等待队列,直接尝试获取锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 如果当前线程已经持有锁,允许重入 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; setState(nextc); return true; } return false; } ``` 在非公平锁的实现中,`hasQueuedPredecessors()` 被跳过,线程可以直接尝试获取锁,而不用等待队列中的线程先执行。这样非公平锁的抢占能力就显得更强。 ### 5. **公平锁与非公平锁的差异** 1. **获取锁时的策略不同**: - **公平锁**:每次获取锁时都会检查等待队列,如果有其他线程在等待锁,当前线程必须进入队列排队,按照顺序获得锁。 - **非公平锁**:当前线程可以直接尝试获取锁,不管是否有其他线程在等待。只有在抢占失败时,才会进入等待队列。 2. **性能差异**: - **非公平锁的性能通常更高**,因为它允许线程直接获取锁,减少了排队等待的开销。特别是在高并发场景下,非公平锁可以提高吞吐量。 - **公平锁的性能较差**,因为它严格按照顺序获取锁,每次锁的释放和获取都会引发上下文切换,容易导致系统开销增大。 3. **线程饥饿问题**: - **非公平锁**可能导致**线程饥饿**。因为每次新来的线程都可能抢到锁,导致等待时间长的线程一直得不到执行机会。 - **公平锁**不会导致线程饥饿,因为所有线程都会按照进入队列的顺序依次获取锁。 ### 6. **什么时候使用公平锁和非公平锁?** - **公平锁**适合**需要严格保证任务执行顺序**的场景,比如任务的执行顺序对业务逻辑有重要影响时,可以使用公平锁来确保不会发生“插队”。 - **非公平锁**更适合**追求性能**的场景,尤其是在高并发下,非公平锁可以通过减少排队和上下文切换来提高吞吐量,虽然可能会导致部分线程长时间得不到锁(线程饥饿问题)。 ### 总结 - **公平锁**严格按照线程进入锁的顺序获取锁,确保等待时间最长的线程优先获取。 - **非公平锁**不考虑线程的排队顺序,允许当前线程直接抢占锁,性能较高,但可能导致线程饥饿。 `ReentrantLock` 通过其内部的 `FairSync` 和 `NonfairSync` 类分别实现了公平锁和非公平锁。公平锁通过检查等待队列确保按顺序获取锁,而非公平锁允许线程直接尝试获取锁。发布于 2024-10-04 23:49 •IP属地 重庆举报评论收藏
公众号进入网站体验更佳

