公平锁与非公平锁的对比

mac2026-04-14  0

扫描下方二维码或者微信搜索公众号菜鸟飞呀飞,即可关注微信公众号,阅读更多Spring源码分析和Java并发编程文章。

1. 问题

在上一篇文章中结合源码介绍了公平锁和非公平锁的实现【文章链接】。这一篇文章将从公平性和性能方面对比一下两者。在阅读本文之前,可以先思考一下下面两个问题。1. 非公平锁一定不公平吗?2. 公平锁与非公平锁的性能谁更好?

2. 对比

主要从公平性和性能这两个方面来对比一下公平锁和非公平锁。

2.1 公平性

在上一篇文章的总结处,提到了公平锁和非公平锁是从线程获取锁所等待的时间来区分两者的公平性。公平锁中,多个线程抢锁时,获取到锁的线程一定是同步队列中等待时间最长的线程。而非公平锁中,多个线程抢锁时,获取锁的线程不一定是同步队列中等待时间最长的线程,有可能是同步队列之外的线程先抢到锁。

对于公平锁,线程获取锁的过程可以用如下示意图表示(图片来源于公众号:Mr羽墨青衫,文章链接:深入剖析Java重入锁ReentrantLock的实现原理)。

从图中可以发现,当持有锁的线程T1释放锁以后,会唤醒同步队列中的T2线程,只要同步队列中有线程在等待获取锁,那么其他刚进来想要获取锁的人,就不能插队,包括T1自己还想要获取锁,也需要去排队,这样就保证了让等待时间最长的线程获取到锁,即保证了公平性。

对于非公平锁,线程获取锁的示意图可以用如下示意图表示。(图片来源于公众号:Mr羽墨青衫,文章链接:深入剖析Java重入锁ReentrantLock的实现原理)。

从图中可以发现,当持有锁的线程T1释放锁以后,会唤醒同步队列中的T2线程,此时即使同步队列中有线程在排队,从外面刚进来的线程想要获取锁,此时是可以直接去争抢锁的,包括线程T1自己,这个时候就是T2、T1、外面刚进来的线程一起去抢锁,虽然此时T2线程等待的时间最长,但是不能保证T2一定抢到锁,所以此时是不公平的。

文章开头提到了一个问题,非公平锁一定不公平吗?答案是不一定,对于刚刚上面图中展示的那种情况,此时非公平锁是不公平的。但是存在一种特殊情况,可以参考如下示意图,如当T1线程释放锁以后,AQS同步队列外部没有线程来争抢锁,T1线程在释放锁以后,自己也不需要获取锁了,此时T1因为唤醒的是T2,现在是有T2一个线程来抢锁,所以此时能获取到锁的线程一定是T2,这种情况下,非公平锁又是公平的了,因为此时即使同步队列中有T3、T4、…、Tn线程,但是T2等待的时间最长,所以是T2获取到锁(AQS的同步队列遵循的原则是FIFO)。

从非公平锁的示意图中,我们也可以发现,如果线程一旦获取锁失败进入到AQS同步队列后,就会一直排队,直到其他获取到锁的线程唤醒它或者它被其他线程中断,才会出队。即:一朝排队,永远排队。

性能

公平锁和非公平锁的性能是不一样的,非公平锁的性能会优于公平锁。为什么呢?因为公平锁在获取锁时,永远是等待时间最长的线程获取到锁,这样当线程T1释放锁以后,如果还想继续再获取锁,它也得去同步队列尾部排队,这样就会频繁的发生线程的上下文切换,当线程越多,对CPU的损耗就会越严重。非公平锁性能虽然优于公平锁,但是会存在导致线程饥饿的情况。在最坏的情况下,可能存在某个线程一直获取不到锁。不过相比性能而言,饥饿问题可以暂时忽略,这可能就是ReentrantLock默认创建非公平锁的原因之一了。下面以一个demo为例,对比了一下公平锁与非公平锁的性能。 public class Demo { // 公平锁 private static Lock fairLock = new ReentrantLock(true); // 非公平锁 private static Lock nonFairLock = new ReentrantLock(false); // 计数器 private static int fairCount = 0; // 计数器 private static int nonFairCount = 0; public static void main(String[] args) throws InterruptedException { System.out.println("公平锁耗时: " + testFairLock(10)); System.out.println("非公平锁耗时: " + testNonFairLock(10)); System.out.println("公平锁累加结果: " + fairCount); System.out.println("非公平锁累加结果: " + nonFairCount); } public static long testFairLock(int threadNum) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(threadNum); // 创建threadNum个线程,让其以公平锁的方式,对fairCount进行自增操作 List<Thread> fairList = new ArrayList<>(); for (int i = 0; i < threadNum; i++) { fairList.add(new Thread(() -> { for (int j = 0; j < 10000; j++) { fairLock.lock(); fairCount++; fairLock.unlock(); } countDownLatch.countDown(); })); } long startTime = System.currentTimeMillis(); for (Thread thread : fairList) { thread.start(); } // 让所有线程执行完 countDownLatch.await(); long endTime = System.currentTimeMillis(); return endTime - startTime; } public static long testNonFairLock(int threadNum) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(threadNum); // 创建threadNum个线程,让其以非公平锁的方式,对nonFairCountCount进行自增操作 List<Thread> nonFairList = new ArrayList<>(); for (int i = 0; i < threadNum; i++) { nonFairList.add(new Thread(() -> { for (int j = 0; j < 10000; j++) { nonFairLock.lock(); nonFairCount++; nonFairLock.unlock(); } countDownLatch.countDown(); })); } long startTime = System.currentTimeMillis(); for (Thread thread : nonFairList) { thread.start(); } // 让所有线程执行完 countDownLatch.await(); long endTime = System.currentTimeMillis(); return endTime - startTime; } } 上面的Demo中,创建了threadNum个线程,然后让这threadNum个线程并发的对变量进行累加10000次的操作,分别用公平锁和非公平锁来保证线程安全,最后分别统计出公平锁和非公平锁的耗时结果。当threadNum = 10时,重复三次测试的结果如下。 次数公平锁非公平锁1618ms22ms2544ms20ms3569ms15ms 当threadNum = 20时,重复三次测试的结果如下。 次数公平锁非公平锁11208ms25ms21146ms26ms31215ms19ms 当threadNum = 30时,重复三次测试的结果如下。 次数公平锁非公平锁11595ms28ms21543ms31ms31601ms31ms 测试环境:macOS 10.14.6,处理器:2.9GHZ,Intel Core i7,内存:16G 2133MHz从上面的测试结果可以发现,非公平锁的耗时远远小于公平锁的耗时,这说明非公平锁在并发情况下,性能更好,吞吐量更大。当线程数越多时,差异越明显。

相关推荐

管程:并发编程的基石初识CAS的实现原理Unsafe类的源码解读以及使用场景队列同步器(AQS)的设计原理队列同步器(AQS)源码分析可重入锁(ReentrantLock)源码分析通过源码看Bean的创建过程Spring源码系列之容器启动流程Spring中最!最!最!重要的后置处理器!没有之一!!!@Import和@EnableXXX手写一个Redis和Spring整合的插件为什么JDK的动态代理要基于接口实现而不能基于继承实现?FactoryBean——Spring的扩展点之一@Autowired注解的实现原理一次策略设计模式的实际应用Spring如何解决循环依赖

最新回复(0)