公平锁和非公平锁有何区别?请解释一下。
参考回答
公平锁和非公平锁的区别在于线程获取锁的顺序是否按照请求的先后顺序:
- 公平锁:按照线程请求锁的顺序来分配锁。即先等待的线程会先获得锁,类似于排队机制,公平性更好,但性能可能较低。
- 非公平锁:线程在竞争锁时可能会“插队”。即后来的线程有可能直接获取到锁,而不一定按照请求的顺序分配,性能更高,但可能造成某些线程长时间得不到锁(线程饥饿)。
在 Java 中:
ReentrantLock
默认是非公平锁,但可以通过构造方法指定为公平锁(new ReentrantLock(true)
)。synchronized
的实现是非公平的。
详细讲解与拓展
1. 公平锁与非公平锁的工作机制
- 公平锁的机制:
- 锁维护一个等待队列,线程按照请求锁的顺序排队。
- 当锁可用时,优先分配给队列中等待时间最长的线程。
- 优点:线程按照请求顺序获取锁,避免线程饥饿。
- 缺点:由于需要维护队列,且每次请求都需要检查队列状态,性能相对较低。
- 非公平锁的机制:
- 线程在尝试获取锁时,不检查等待队列,直接尝试抢占锁。
- 优点:锁的获取速度更快,吞吐量更高。
- 缺点:可能导致某些线程长时间得不到锁(线程饥饿)。
2. 示例代码
公平锁:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private final Lock lock = new ReentrantLock(true); // 公平锁
public void print() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
FairLockExample example = new FairLockExample();
Runnable task = example::print;
for (int i = 0; i < 5; i++) {
new Thread(task, "Thread-" + i).start();
}
}
}
非公平锁:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class NonFairLockExample {
private final Lock lock = new ReentrantLock(); // 非公平锁(默认)
public void print() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
NonFairLockExample example = new NonFairLockExample();
Runnable task = example::print;
for (int i = 0; i < 5; i++) {
new Thread(task, "Thread-" + i).start();
}
}
}
输出差异:
- 公平锁的输出严格按照线程的启动顺序。
- 非公平锁的输出顺序可能无序,因为后启动的线程可能会插队。
3. 性能对比
- 公平锁:
- 性能较低,因为需要维护队列并进行顺序调度。
- 更适合高可靠性场景,例如对实时性或公平性有严格要求的系统。
- 非公平锁:
- 性能较高,因为减少了线程调度的开销。
- 更适合高并发场景,例如对吞吐量要求较高的系统。
4. 公平锁和非公平锁的适用场景
- 公平锁:
- 适合需要严格控制锁分配顺序的场景,如打印日志、多线程任务调度。
- 避免了线程饥饿问题,但可能导致整体性能下降。
- 非公平锁:
- 适合对性能要求较高的场景,如数据库连接池、缓存系统。
- 提高吞吐量,但可能出现线程饥饿。
5. JVM 对 synchronized
和 ReentrantLock
的实现
synchronized
:
- 默认是非公平锁,允许插队以提高性能。
- JVM 会对其进行优化(如偏向锁、轻量级锁),进一步提升性能。
ReentrantLock
:
- 默认是非公平锁,但可以通过构造方法指定为公平锁:
“`java
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock nonFairLock = new ReentrantLock(false); // 非公平锁
“`
6. 公平锁与非公平锁的关键实现区别(源码分析)
ReentrantLock
的实现核心在于:
- 公平锁 使用
FairSync
内部类。 - 非公平锁 使用
NonFairSync
内部类。
非公平锁的核心源码:
final boolean nonfairTryAcquire(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;
}
公平锁的核心源码:
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;
setState(nextc);
return true;
}
return false;
}
关键点:
- 公平锁通过
hasQueuedPredecessors()
检查当前线程是否有前置线程,如果有,则必须等待。 - 非公平锁不检查等待队列,允许插队,提高性能。
总结
特性 | 公平锁 | 非公平锁 |
---|---|---|
锁分配顺序 | 严格按照线程请求的顺序分配锁 | 不保证顺序,可能发生线程插队 |
性能 | 较低(需要维护等待队列) | 较高(无需维护等待队列) |
线程饥饿 | 不会发生 | 可能发生 |
适用场景 | 需要严格公平性,如多线程任务调度 | 高并发场景,对性能要求较高 |
实现方式 | ReentrantLock(true) |
ReentrantLock(false) (默认) |
- 公平锁 保证顺序,但性能较低。
- 非公平锁 提升性能,但可能导致线程饥饿。选择哪种锁需要根据具体场景权衡公平性与性能的需求。