扫描下方二维码或者微信搜索公众号
菜鸟飞呀飞
,即可关注微信公众号,阅读更多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
时,重复三次测试的结果如下。
次数 | 公平锁 | 非公平锁 |
---|---|---|
1 | 618ms | 22ms |
2 | 544ms | 20ms |
3 | 569ms | 15ms |
- 当
threadNum = 20
时,重复三次测试的结果如下。
次数 | 公平锁 | 非公平锁 |
---|---|---|
1 | 1208ms | 25ms |
2 | 1146ms | 26ms |
3 | 1215ms | 19ms |
- 当
threadNum = 30
时,重复三次测试的结果如下。
次数 | 公平锁 | 非公平锁 |
---|---|---|
1 | 1595ms | 28ms |
2 | 1543ms | 31ms |
3 | 1601ms | 31ms |
- 测试环境:macOS 10.14.6,处理器:2.9GHZ,Intel Core i7,内存:16G 2133MHz
- 从上面的测试结果可以发现,
非公平锁的耗时远远小于公平锁的耗时
,这说明非公平锁在并发情况下,性能更好,吞吐量更大
。当线程数越多时,差异越明显。