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

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

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

① 准备入队列的节点,此情况讨论的是线程加入等待队列时产生的竞争是否公平,线程在尝试获取锁失败后将被加入等待队列,这时多个线程通过自旋将节点加入队列,所有线程在自旋过程中是无法保证其公平性的,可能后来的线程比早到的先进入队列,所以节点入队列不具公平性。
        ② 等待队列中的节点,情况①中成功加入队列后即成为等待队列中的节点,我们知道此队列是一个先入先出队列,那么很简单能得到,队列中的所有节点是公平的,他们都按照顺序等待自己被前驱节点唤醒并获取锁,所以等待队列中的节点具有公平性。
        ③ 闯入的节点,这种情况是指一个新线程到达共享资源边界时不管等待队列中是否存在其他等待节点它都将优先尝试去获取锁,这种称为可闯入策略。可闯入特性破坏了公平性,JDK的AQS对外体现的公平性主要由此体现,下面将对闯入特性展开分析。
        AQS提供的基础获取锁算法是一种可闯入的算法,即如果有新线程到来先进行一次获取尝试,不成功的情况下才将当前线程加入等待队列。如图2-5-9-6所示,等待队列中节点线程按照顺序一个接一个尝试去获取共享资源的使用权,某时刻头结点线程准备尝试获取的同时另外一条线程闯入,此线程并非直接加入等待队列的尾部,而是先跟头结点线程竞争获取资源,闯入线程如果成功获取共享资源则直接执行,头结点线程则继续等待下一次尝试,如此一来闯入线程成功插队,后来的线程比早到的线程先执行,说明AQS基础获取算法是不严格公平的。

 

图2-5-9-6 闯入线程

基础获取算法逻辑简化如下:首先尝试获取锁,假如获取失败才创建节点并加入到等待队列的尾部,接着通过不断循环检查是否轮到自己执行,当然此过程为了提高性能可能将线程先挂起,最终由前驱节点唤醒。
if(尝试获取锁失败) {
    创建node
    使用CAS方式把node插入到队列尾部
    while(true){
    if(尝试获取锁成功 并且 node的前驱节点为头节点){
把当前节点设置为头节点
    跳出循环
}else{
    使用CAS方式修改node前驱节点的waitStatus标识为signal
    if(修改成功)
        挂起当前线程 
}
}
        为什么要使用闯入策略?可闯入的策略通常可以提供更高的总吞吐量。由于一般同步器颗粒度比较小,也可以说共享资源的范围较小,而线程从阻塞状态到被唤醒所消耗的时间周期可能是通过共享资源时间周期的几倍甚至几十倍,如此一来线程唤醒过程中将存在一个很大的时间周期空窗期,导致资源没有得到充分利用,为了提高吞吐量,引入这种闯入策略,它可以使在等待队列头结点从阻塞到被唤醒的时间段内闯入的线程直接获取锁并通过同步器,以便充分利用唤醒过程这一空窗期,大大增加了吞吐率。另外,闯入机制的实现对外提供一种竞争调节机制,即开发者可以在自定义同步器中定义闯入尝试获取的次数,假设次数为n则不断重复获取直到n次都获取不成功才把线程加入等待队列中,随着次数n的增加可以增大成功闯入的几率。同时,这种闯入策略可能导致等待队列中的线程饥饿,因为锁可能一直被闯入的线程获取,但由于一般持有同步器的时间很短暂而避免饥饿的发生,反之如果保护的代码体很长并且持有同步器的时间较长,这将大大增加等待队列无限等待的风险。

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

喜欢研究java的同学可以交个朋友,下面是本人的微信号:

时间: 2024-08-29 15:30:07

从JDK源码角度看并发的公平性的相关文章

从JDK源码角度看java并发的原子性如何保证

JDK源码中,在研究AQS框架时,会发现很多地方都使用了CAS操作,在并发实现中CAS操作必须具备原子性,而且是硬件级别的原子性,java被隔离在硬件之上,明显力不从心,这时为了能直接操作操作系统层面,肯定要通过用C++编写的native本地方法来扩展实现.JDK提供了一个类来满足CAS的要求,sun.misc.Unsafe,从名字上可以大概知道它用于执行低级别.不安全的操作,AQS就是使用此类完成硬件级别的原子操作. Unsafe是一个很强大的类,它可以分配内存.释放内存.可以定位对象某字段的

从源码角度看Android系统SystemServer进程启动过程

copy frome :https://blog.csdn.net/salmon_zhang/article/details/93208135 SystemServer进程是由Zygote进程fork生成,进程名为system_server,主要用于创建系统服务. 备注:本文将结合Android8.0的源码看SystemServer进程的启动过程以及SystemServer进程做了哪些重要工作. 1. SystemServer进程启动的起点从<从源码角度看Android系统Zygote进程启动过

Android布局性能优化—从源码角度看ViewStub延迟加载技术

在项目中,难免会遇到这种需求,在程序运行时需要动态根据条件来决定显示哪个View或某个布局,最通常的想法就是把需要动态显示的View都先写在布局中,然后把它们的可见性设为View.GONE,最后在代码中通过控制View.VISIABLE动态的更改它的可见性.这样的做法的优点是逻辑简单而且控制起来比较灵活.但是它的缺点就是,耗费资源,虽然把View的初始可见View.GONE但是在Inflate布局的时候View仍然会被Inflate,也就是说仍然会创建对象,会被实例化,会被设置属性.也就是说,会

从源码角度看finish()方法的执行流程

1. finish()方法概览 首先我们来看一下finish方法的无参版本的定义: /** * Call this when your activity is done and should be closed. The * ActivityResult is propagated back to whoever launched you via * onActivityResult(). */ public void finish() { finish(false); } 根据源码中的注释我们

从源码角度看一个apk的启动过程和一个activity的启动过程

APK程序的运行过程 首先,ActivityThread从main()函数中开始执行,调用prepareMainLooper()为UI线程创建一个消息队列(MessageQueue). 然后创建一个ActivityThread对象,在ActivityThread的初始化代码中会创建一个H(Handler)对象和一个ApplicationThread(Binder)对象.其中Binder负责接收远程AmS的IPC调用,接收到调用后,则通过Handler把消息发送到消息队列,UI主线程会异步地从消息

Netbeans、Eclipse中查看JDK源码

单纯的会使用JDK里的API似乎还不足以成为猿类,你可以轻松一点,进入到JDK源码里看个究竟.下面说明在Netbeans.Eclipse环境下怎么查看JDK源码: Netbeans: 在"工具->java平台->源"里添加下路径,如果你安装jdk的时候选择安装了源码的话,jdk目录里会有一个src.zip的文件,添加这个文件就可以了. 选中相应的代码,ctrl+鼠标左键.就进入到源码环境了. Eclipse: 1.点 “window”-> "Preferen

解决debug到jdk源码时不能查看变量值的问题

目录 如何跟踪jdk源码 1. 编译源码 2. 关联源码 3. 大功告成 如何跟踪jdk源码 看到这个标题大概大家都会在心里想谁还跟踪个源码呀,在eclipse中打个断点,以debug的方式运行,然后F5进入方法,F6跳过方法,F7跳出方法.但是不知道大家有没有注意到,如果你跟踪到的是jdk源码的话,比如HashMap的put方法,即使你F5进入到这个方法的内部了,你也看不到你put的 key 和 value 的实际值.但是我们既然要跟踪源码,那么肯定要看到我们设置的 key 和 value 是

JDK源码那些事儿之并发ConcurrentHashMap上篇

前面前已经说明了HashMap以及红黑树的一些基本知识,对JDK8的HashMap也有了一定的了解,本篇就开始看看并发包下的ConcurrentHashMap,说实话,还是比较复杂的,笔者在这里也不会过多深入,源码层次上了解一些主要流程即可,清楚多线程环境下整个Map的运作过程就算是很大进步了,更细的底层部分需要时间和精力来研究,暂不深入 前言 jdk版本:1.8 JDK7中,ConcurrentHashMap把内部细分成了若干个小的HashMap,称之为段(Segment),默认被分为16个段

myeclipse调试代码的时候看不到变量的值和jdk源码重新编译

Q:myeclipse调试代码的时候看不到变量的值? A: 调试的类的Class文件里没有包含LocalVariableTable属性表, 这是因为在编译时,编译者为了减少文件大小而不把属性表的信息放在Class文件中. 使用命令为javac -g>http://hllvm.group.iteye.com/group/topic/25798--解决方法:需要重新编译一份用于debug版的. Q:重新编译jdk源码,启用debug信息 >http://www.cnblogs.com/thecat