【锁思想】为什么synchronized的默认策略是非公平的?
大家好,我是Coder哥,在技术日新月异的今天,真正应该花费时间学习的是那些不变的编程思想,那么今天我们接着上一篇,接着聊锁,我们来聊一下公平锁和非公平锁。 前几天看到一个问题”为什么synchronized是非公平的“,我仔细的思考了一下,发现不只是synchronized, ReentrantLock 的默认策略也是非公平的,非公平是实现锁的一种策略,不只是Java,其他语言的默认锁机制也都是非公平的,那么今天我们来详细的聊一下 ”为什么各种语言中锁实现的默认策略都是非公平的“ 。这个非公平是真的完全不公平随机获取的么?
公平和非公平
首先,我们来看下什么是公平锁和非公平锁,为了能让大家更清楚,我们用图例来说明一下公平和非公平的场景。
公平锁的场景
我们先看一下公平锁的场景,顾名思义,公平锁指的就是按照线程请求的顺序来分配锁; 比如,我们给临界区加了一个公平锁,此时有3个线程先后来请求锁,线程1先到,就会先获得锁,那么线程2,3会在队列中等待,等线程1释放锁后,线程2,3会依次去获得锁,如果此时有线程4来竟争锁,会排在线程2,3的后面等待。
然后等线程 1 释放锁之后,线程 2、3、4 会依次去获取这把锁,线程 2 先获取到的原因是它等待的时间最长。
非公平锁的场景
非公平锁指的是不按照顺序来分配,在一定的情况下,可以插队。这里需要注意的是:
这里的非公平并不是完全随机的,也不是可以任意插队,而是在合适的时机插队。
那么什么是合适的时机呢? 比如线程1执行完毕的时候,此时线程2,3,4在队列里面,这时候线程5过来请求锁,刚好线程1释放锁,那么当前的锁就会给到线程5,而不是线程2,这就是所谓的合适的时机插队,如图:
然后等线程5执行完毕后,如果有线程6也恰巧过来,那么这个锁会给到线程6,如果没有这个恰巧的话就会给到线程2.
我知道你有疑问了,按这个逻辑如果恰巧线程7,线程8,线程9... ,那么线程2会一直等待,对,这就是非公平锁造成的线程饥饿。 这也是非公平锁的缺点
看到这里,你可能更加的疑惑了,非公平锁有这样可能造成饥饿的缺点,那么为什么几乎所有语言层面的默认锁机制都是非公平策略呢?难道我们这些排队的时间都白白浪费了吗?排了半天被别人插队?这就引出了我们文章开头的问题:为什么各种语言中锁实现的默认策略都是非公平的?
为什么各种语言中锁实现的默认策略都是非公平的?
都用非公平策略是有原因的,比如线程1持有一把锁,这个时候线程2,3,4依次请求进来,那么他们依次排队到队列,陷入等待,也就是进入阻塞的状态,然后线程1执行完毕,本该轮到线程2苏醒获取到锁,但是这个时候恰巧线程5请求这把锁,那么根据非公平的原则,线程5就获取到锁了,这是因为唤醒线程2会有很大的开销,因为程序的执行大部分都很快,很可能在唤醒线程2之前,线程5就已经执行完毕了,所以按照非公平策略的逻辑,这里会让线程5先获取到锁,相比于等待线程2唤醒的漫长过程,直接执行线程5效率会更高,这是一个双赢的局面。
基于上面的场景有很多好处
对于线程5而言: 不需要任何等待直接获取到锁并执行,提高了它的效率。
对于线程2而言:它获得锁的时间并没有推迟,因为等它被唤醒的时候,线程 5 早就释放锁了,因为线程 5 的执行速度相比于线程 2 的唤醒速度,是很快的。
所以一般情况下锁的默认策略,都是非公平的策略,这是为了提高整体的运行效率。
公平和非公平的优缺点
我们接下来看一下公平和非公平的优缺点,如表格所示。
公平锁的优点在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会,而它的缺点在于整体执行速度更慢,吞吐量更小,而非公平锁的优势就在于整体执行速度更快,吞吐量更大,但同时也可能产生线程饥饿问题,也就是说如果一直有线程插队,那么在等待队列中的线程可能长时间得不到运行。
最后
综上所述,公平锁就是会按照多个线程申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待情况,直接尝试获取锁,这样会存在插队先获得锁的情况,但这样也提高了整体的效率,吞吐量更大,执行更快。
书籍推荐:
《计算机内功修炼系列》:https://www.todocoder.com/pdf/jichu/001001.html
《Java编程思想》 :https://www.todocoder.com/pdf/java/002002.html
《Java并发编程实战》 :https://www.todocoder.com/pdf/java/002004.html