面试突击:公平锁和非公平锁有什么区别?

开发 后端
在 Java 语言中,锁的默认实现都是非公平锁,原因是非公平锁的效率更高,使用 ReentrantLock 可以手动指定其为公平锁。非公平锁注重的是性能,而公平锁注重的是锁资源的平均分配,所以我们要选择合适的场景来应用二者。

作者 | 磊哥

来源 | Java面试真题解析(ID:aimianshi666)

转载请联系授权(微信ID:GG_Stone)

从公平的角度来说,Java 中的锁总共可分为两类:公平锁和非公平锁。但公平锁和非公平锁有哪些区别?孰优孰劣呢?在 Java 中的应用场景又有哪些呢?接下来我们一起来看。

正文

公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。举个例子,公平锁就像开车经过收费站一样,所有的车都会排队等待通过,先来的车先通过,如下图所示:

通过收费站的顺序也是先来先到,分别是张三、李四、王五,这种情况就是公平锁。而非公平锁相当于,来了一个强行加塞的老司机,它不会准守排队规则,来了之后就会试图强行加塞,如果加塞成功就顺利通过,当然也有可能加塞失败,如果失败就乖乖去后面排队,这种情况就是非公平锁。

应用场景

在 Java 语言中,锁 synchronized 和 ReentrantLock 默认都是非公平锁,当然我们在创建 ReentrantLock 时,可以手动指定其为公平锁,但 synchronized 只能为非公平锁。ReentrantLock 默认为非公平锁可以在它的源码实现中得到验证,如下源码所示:

当使用 new ReentrantLock(true) 时,可以创建公平锁,如下源码所示:

当使用 new ReentrantLock(true) 时,可以创建公平锁,如下源码所示:

公平和非公平锁代码演示

接下来我们使用 ReentrantLock 来演示一下公平锁和非公平锁的执行差异,首先定义一个公平锁,开启 3 个线程,每个线程执行两次加锁和释放锁并打印线程名的操作,如下代码所示:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockFairTest {
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int j = 0; j < 2; j++) {
lock.lock();
System.out.println("当前线程:" + Thread.currentThread()
.getName());
lock.unlock();
}
}).start();
}
}
}

以上程序的执行结果如下图所示:

接下来我们使用非公平锁来执行上面的代码,具体实现如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockFairTest {
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int j = 0; j < 2; j++) {
lock.lock();
System.out.println("当前线程:" + Thread.currentThread()
.getName());
lock.unlock();
}
}).start();
}
}
}

以上程序的执行结果如下图所示:

从上述结果可以看出,使用公平锁线程获取锁的顺序是:A -> B -> C -> A -> B -> C,也就是按顺序获取锁。而非公平锁,获取锁的顺序是 A -> A -> B -> B -> C -> C,原因是所有线程都争抢锁时,因为当前执行线程处于活跃状态,其他线程属于等待状态(还需要被唤醒),所以当前线程总是会先获取到锁,所以最终获取锁的顺序是:A -> A -> B -> B -> C -> C。

执行流程分析

公平锁执行流程

获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。

非公平锁执行流程

当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。公平锁和非公平锁的性能测试结果如下,以下测试数据来自于《Java并发编程实战》:

从上述结果可以看出,使用非公平锁的吞吐率(单位时间内成功获取锁的平均速率)要比公平锁高很多。

优缺点分析

公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,不会“按资排辈”以及顺序唤醒,但缺点是资源分配随机性强,可能会出现线程饿死的情况。

总结

在 Java 语言中,锁的默认实现都是非公平锁,原因是非公平锁的效率更高,使用 ReentrantLock 可以手动指定其为公平锁。非公平锁注重的是性能,而公平锁注重的是锁资源的平均分配,所以我们要选择合适的场景来应用二者。

责任编辑:姜华 来源: Java面试真题解析
相关推荐

2022-07-12 08:56:18

公平锁非公平锁Java

2022-12-26 00:00:04

公平锁非公平锁

2023-10-07 08:17:40

公平锁非公平锁

2019-01-04 11:18:35

独享锁共享锁非公平锁

2023-03-26 21:51:42

2020-08-24 08:13:25

非公平锁源码

2018-07-31 15:05:51

Java公平锁线程

2021-08-20 07:54:20

非公平锁 Java多线编程

2022-08-22 07:06:32

MyBatisSQL占位符

2022-08-03 07:04:56

GETHTTPPOST

2022-08-10 07:06:57

IoCDISpring

2022-04-24 07:59:53

synchronizJVMAPI

2022-08-15 07:06:50

Propertiesyml配置

2022-02-08 07:02:32

进程线程操作系统

2021-06-02 21:31:39

Synchronous非公平模式

2022-04-26 08:02:00

locktryLocklockInterr

2021-07-02 08:51:09

Redisson分布式锁公平锁

2022-12-08 17:15:54

Java并发包

2022-10-09 20:52:19

事务隔离级别传播机制

2021-06-30 14:56:12

Redisson分布式公平锁
点赞
收藏

51CTO技术栈公众号