【锁思想】为什么synchronized的默认策略是非公平的?

3552 篇文章 111 订阅

在技术日新月异的今天,真正应该花费时间学习的是那些不变的编程思想,那么今天我们来聊一下公平和非公平策略的思想,前几天看到一个问题”为什么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 的唤醒速度,是很快的。

所以一般情况下锁的默认策略,都是非公平的策略,这是为了提高整体的运行效率。

公平和非公平的优缺点

我们接下来看一下公平和非公平的优缺点,如表格所示。

公平锁的优点在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会,而它的缺点在于整体执行速度更慢,吞吐量更小,而非公平锁的优势就在于整体执行速度更快,吞吐量更大,但同时也可能产生线程饥饿问题,也就是说如果一直有线程插队,那么在等待队列中的线程可能长时间得不到运行。

结合源码分析

结合ReentrantLock的源码我们来分析公平和非公平锁是如何实现的,查阅源码可以看到在 ReentrantLock 类包含一个 Sync 类,这个类继承自AQS(AbstractQueuedSynchronizer),代码如下:

java复制代码public class ReentrantLock implements Lock {
 
private final Sync sync;
// Sync 类的代码:
abstract static class Sync extends AbstractQueuedSynchronizer {...}
  ...
// 非公平锁
static final class NonfairSync extends Sync {...}
// 公平锁
static final class FairSync extends Sync {...}
}

根据代码可知,Sync 有公平锁 FairSync 和非公平锁 NonfairSync两个子类,下面我们来看一下公平锁与非公平锁的加锁方法的源码。

公平锁的锁获取源码如下:

java复制代码protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    		//这里判断了 hasQueuedPredecessors(),
      	// 判断在等待队列中是否已经有线程在排队了
        if (!hasQueuedPredecessors() && 
                compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
      	// 这里是可重入锁特性的实现,比较简单易懂,这里不做过多探讨,有兴趣可自行查阅源码。
        ...
        return true;
    }
    return false;
}

非公平锁获取锁源码如下:

java复制代码final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
      	//这里没有判断 hasQueuedPredecessors(), 
      	// 也就是直接自旋修改状态,能改就插队了
        if (compareAndSetState(0, acquires)) { 
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        ...
        return true;
    }
    return false;
}

上面代码对比,可以明显的看出公平锁与非公平锁的唯一的区别就在于公平锁在获取锁时多了一个限制条件:就是hasQueuedPredecessors() 判断在等待队列中是否已经有线程在排队了。这也就是公平锁和非公平锁的核心区别,如果是公平锁,那么一旦已经有线程在排队了,当前线程就不再尝试获取锁;对于非公平锁,无论是否已经有线程在排队,都会尝试获取一下锁,获取不到的话,再去排队。

**注意:**在公平锁中,有一个特例需要我们注意一下, tryLock() 方法,它不遵守设定的公平原则。

当有线程执行 tryLock() 方法的时候,会尝试一下是否能插队,也就是类似于非公平锁的调度机制,我们可以看一下源码:

java复制代码public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

这里调用的就是 nonfairTryAcquire(),表明了是不公平的,和锁本身是否是公平锁无关。

最后

综上所述,公平锁就是会按照多个线程申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待情况,直接尝试获取锁,这样会存在插队先获得锁的情况,但这样也提高了整体的效率,吞吐量更大,执行更快。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
synchronized为什么是非公平的呢?通过阅读引用1和引用2,我们可以得出结论。在Java中,synchronized关键字的实现有两种方式:公平和非公平公平会按照线程的请求顺序来获取,而非公平则允许插队,即当前线程在获取时可以抢占已经等待时间很长的线程的synchronized关键字默认使用非公平的原因是为了提高程序的性能。公平需要维护一个有序的等待队列,这会增加系统的开销。而非公平则不需要维护等待队列的顺序,所以相对更加高效。此外,非公平还可以减少线程切换的次数,从而提高程序的执行效率。 需要注意的是,公平并不代表绝对的公平性,也不是完全按照请求的顺序获取。即使是公平,当一个线程释放时,下一个获取的线程也不一定是等待时间最长的线程,因为存在一些运行时的优化机制,比如线程的优先级等。 综上所述,synchronized是非公平的主要是出于性能方面的考虑,非公平的机制能够更高效地处理的竞争,并减少线程切换的次数。在实际应用中,我们需要根据具体情况来选择合适的模式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [【思想】为什么synchronized默认策略是非公平的?](https://blog.csdn.net/m0_73311735/article/details/131304965)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [为什么Synchronized 是非公平?](https://blog.csdn.net/m0_71777195/article/details/131241525)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值