从 JDK 源码角度看 java 并发的公平性

Java为简化开发者开发提供了不少并发的工具,包括各类同步器,有了JDK咱们只要学会简单使用类API便可。但这并不意味着不须要探索其具体的实现机制,本文从JDK源码角度简单讲讲并发时线程竞争的公平性。javascript

所谓公平性指全部线程对临界资源申请访问权限的成功率都同样,不会让某些线程拥有优先权。咱们知道CLH Node FIFO等待队列是一个先进先出的队列,那么是否就能够说每条线程获取锁时就是公平的呢?关于公平性这里分拆成三个点分别阐述:java

  1. 准备入队列的节点,此状况讨论的是线程加入等待队列时产生的竞争是否公平,线程在尝试获取锁失败后将被加入等待队列,这时多个线程经过自旋将节点加入队列,全部线程在自旋过程当中是没法保证其公平性的,可能后来的线程比早到的先进入队列,因此节点入队列不具公平性。
  2. 等待队列中的节点,状况①中成功加入队列后即成为等待队列中的节点,咱们知道此队列是一个先入先出队列,那么很简单能获得,队列中的全部节点是公平的,他们都按照顺序等待本身被前驱节点唤醒并获取锁,因此等待队列中的节点具备公平性。
  3. 闯入的节点,这种状况是指一个新线程到达共享资源边界时无论等待队列中是否存在其余等待节点它都将优先尝试去获取锁,这种称为可闯入策略。可闯入特性破坏了公平性,JDK的AQS对外体现的公平性主要由此体现,下面将对闯入特性展开分析。

AQS提供的基础获取锁算法是一种可闯入的算法,即若是有新线程到来先进行一次获取尝试,不成功的状况下才将当前线程加入等待队列。如图2-5-9-6所示,等待队列中节点线程按照顺序一个接一个尝试去获取共享资源的使用权,某时刻头结点线程准备尝试获取的同时另一条线程闯入,此线程并不是直接加入等待队列的尾部,而是先跟头结点线程竞争获取资源,闯入线程若是成功获取共享资源则直接执行,头结点线程则继续等待下一次尝试,如此一来闯入线程成功插队,后来的线程比早到的线程先执行,说明AQS基础获取算法是不严格公平的。node

基础获取算法逻辑简化以下:首先尝试获取锁,假如获取失败才建立节点并加入到等待队列的尾部,接着经过不断循环检查是否轮到本身执行,固然此过程为了提升性能可能将线程先挂起,最终由前驱节点唤醒。算法

if(尝试获取锁失败) {
    建立node
    使用CAS方式把node插入到队列尾部
    while(true){
    if(尝试获取锁成功 而且 node的前驱节点为头节点){
把当前节点设置为头节点
    跳出循环
}else{
    使用CAS方式修改node前驱节点的waitStatus标识为signal
    if(修改为功)
        挂起当前线程 
}
}复制代码

为何要使用闯入策略?可闯入的策略一般能够提供更高的总吞吐量。因为通常同步器颗粒度比较小,也能够说共享资源的范围较小,而线程从阻塞状态到被唤醒所消耗的时间周期多是经过共享资源时间周期的几倍甚至几十倍,如此一来线程唤醒过程当中将存在一个很大的时间周期空窗期,致使资源没有获得充分利用,为了提升吞吐量,引入这种闯入策略,它可使在等待队列头结点从阻塞到被唤醒的时间段内闯入的线程直接获取锁并经过同步器,以便充分利用唤醒过程这一空窗期,大大增长了吞吐率。另外,闯入机制的实现对外提供一种竞争调节机制,即开发者能够在自定义同步器中定义闯入尝试获取的次数,假设次数为n则不断重复获取直到n次都获取不成功才把线程加入等待队列中,随着次数n的增长能够增大成功闯入的概率。同时,这种闯入策略可能致使等待队列中的线程饥饿,由于锁可能一直被闯入的线程获取,但因为通常持有同步器的时间很短暂而避免饥饿的发生,反之若是保护的代码体很长而且持有同步器的时间较长,这将大大增长等待队列无限等待的风险。并发

在实际状况中仍是要根据用户需求制定策略,在一个公平性要求很高的场景,则能够把闯入策略去除掉以达到公平。在自定义同步器中能够经过AQS预留方法tryAcquire方法实现,只需判断当前线程是否为等待队列中头结点对应的线程,若不是则直接返回false,尝试获取失败。但前面这种公平性是相对Java语法语义层面上的公平性,在现实中JDK的实现会直接影响线程执行的顺序。工具

欢迎关注:性能

相关文章
相关标签/搜索