转载

公平锁与非公平锁的对比

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

公平锁与非公平锁的对比

1. 问题

1.
2.

2. 对比

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

2.1 公平性

  • 在上一篇文章的总结处,提到了公平锁和非公平锁是从线程获取锁所等待的时间来区分两者的公平性。公平锁中,多个线程抢锁时,获取到锁的线程一定是同步队列中等待时间最长的线程。而非公平锁中,多个线程抢锁时,获取锁的线程不一定是同步队列中等待时间最长的线程,有可能是同步队列之外的线程先抢到锁。
  • 对于公平锁,线程获取锁的过程可以用如下示意图表示(图片来源于公众号: Mr羽墨青衫 ,文章链接: 深入剖析Java重入锁ReentrantLock的实现原理 )。
公平锁与非公平锁的对比
  • 从图中可以发现,当持有锁的线程T1释放锁以后,会唤醒同步队列中的T2线程,只要同步队列中有线程在等待获取锁,那么其他刚进来想要获取锁的人,就不能插队,包括T1自己还想要获取锁,也需要去排队,这样就保证了让等待时间最长的线程获取到锁,即保证了公平性。

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

公平锁与非公平锁的对比
非公平锁一定不公平吗?
公平锁与非公平锁的对比
  • 从非公平锁的示意图中,我们也可以发现,如果线程一旦获取锁失败进入到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
  • 从上面的测试结果可以发现, 非公平锁的耗时远远小于公平锁的耗时 ,这说明非公平锁在并发情况下, 性能更好,吞吐量更大 。当线程数越多时,差异越明显。
原文  https://juejin.im/post/5dbcea3bf265da4cf37683e6
正文到此结束
Loading...