JDK1.8 AbstractQueuedSynchronizer的实现分析(上)

2023-11-16

深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上)

  作者 刘锟洋 发布于 2014年7月31日

http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer

前言

Java中的FutureTask作为可异步执行任务并可获取执行结果而被大家所熟知。通常可以使用future.get()来获取线程的执行结果,在线程执行结束之前,get方法会一直阻塞状态,直到call()返回,其优点是使用线程异步执行任务的情况下还可以获取到线程的执行结果,但是FutureTask的以上功能却是依靠通过一个叫AbstractQueuedSynchronizer的类来实现,至少在JDK 1.5、JDK1.6版本是这样的(从1.7开始FutureTask已经被其作者Doug Lea修改为不再依赖AbstractQueuedSynchronizer实现了,这是JDK1.7的变化之一)。但是AbstractQueuedSynchronizer在JDK1.8中还有如下图所示的众多子类:

这些JDK中的工具类或多或少都被大家用过不止一次,比如ReentrantLock,我们知道ReentrantLock的功能是实现代码段的并发访问控制,也就是通常意义上所说的锁,在没有看到AbstractQueuedSynchronizer前,可能会以为它的实现是通过类似于synchronized,通过对对象加锁来实现的。但事实上它仅仅是一个工具类!没有使用更“高级”的机器指令,不是关键字,也不依靠JDK编译时的特殊处理,仅仅作为一个普普通通的类就完成了代码块的并发访问控制,这就更让人疑问它怎么实现的代码块的并发访问控制的了。那就让我们一起来仔细看下Doug Lea怎么去实现的这个锁。为了方便,本文中使用AQS代替AbstractQueuedSynchronizer。

细说AQS

在深入分析AQS之前,我想先从AQS的功能上说明下AQS,站在使用者的角度,AQS的功能可以分为两类:独占功能和共享功能,它的所有子类中,要么实现并使用了它独占功能的API,要么使用了共享锁的功能,而不会同时使用两套API,即便是它最有名的子类ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别实现的两套API来实现的,为什么这么做,后面我们再分析,到目前为止,我们只需要明白AQS在功能上有独占控制和共享控制两种功能即可。

独占锁

在真正对解读AQS之前,我想先从使用了它独占控制功能的子类ReentrantLock说起,分析ReentrantLock的同时看一看AQS的实现,再推理出AQS独特的设计思路和实现方式。最后,再看其共享控制功能的实现。

对于ReentrantLock,使用过的同学应该都知道,通常是这么用它的:

reentrantLock.lock()
        //do something
        reentrantLock.unlock()

ReentrantLock会保证 do something在同一时间只有一个线程在执行这段代码,或者说,同一时刻只有一个线程的lock方法会返回。其余线程会被挂起,直到获取锁。从这里可以看出,其实ReentrantLock实现的就是一个独占锁的功能:有且只有一个线程获取到锁,其余线程全部挂起,直到该拥有锁的线程释放锁,被挂起的线程被唤醒重新开始竞争锁。没错,ReentrantLock使用的就是AQS的独占API实现的。

那现在我们就从ReentrantLock的实现开始一起看看重入锁是怎么实现的。

首先看lock方法:

如FutureTask(JDK1.6)一样,ReentrantLock内部有代理类完成具体操作,ReentrantLock只是封装了统一的一套API而已。值得注意的是,使用过ReentrantLock的同学应该知道,ReentrantLock又分为公平锁和非公平锁,所以,ReentrantLock内部只有两个sync的实现:

公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,类似于排队吃饭。

非公平锁:每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关,类似于堵车时,加塞的那些XXXX。

到这里,通过ReentrantLock的功能和锁的所谓排不排队的方式,我们是否可以这么猜测ReentrantLock或者AQS的实现(现在不清楚谁去实现这些功能):有那么一个被volatile修饰的标志位叫做key,用来表示有没有线程拿走了锁,或者说,锁还存不存在,还需要一个线程安全的队列,维护一堆被挂起的线程,以至于当锁被归还时,能通知到这些被挂起的线程,可以来竞争获取锁了。

至于公平锁和非公平锁,唯一的区别是在获取锁的时候是直接去获取锁,还是进入队列排队的问题了。为了验证我们的猜想,我们继续看一下ReentrantLock中公平锁的实现:

调用到了AQS的acquire方法:

从方法名字上看语义是,尝试获取锁,获取不到则创建一个waiter(当前线程)后放到队列中,这和我们猜测的好像很类似。[G1]

先看下tryAcquire方法:

留空了,Doug Lea是想留给子类去实现(既然要给子类实现,应该用抽象方法,但是Doug Lea没有这么做,原因是AQS有两种功能,面向两种使用场景,需要给子类定义的方法都是抽象方法了,会导致子类无论如何都需要实现另外一种场景的抽象方法,显然,这对子类来说是不友好的。)

看下FairSync的tryAcquire方法:

getState方法是AQS的方法,因为在AQS里面有个叫statede的标志位 :

事实上,这个state就是前面我们猜想的那个“key”!

回到tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();  //获取父类AQS中的标志位
            if (c == 0) {
                if (!hasQueuedPredecessors() && 
                    //如果队列中没有其他线程  说明没有线程正在占有锁!
                    compareAndSetState(0, acquires)) { 
                    //修改一下状态位,注意:这里的acquires是在lock的时候传递来的,从上面的图中可以知道,这个值是写死的1
                    setExclusiveOwnerThread(current);
                    //如果通过CAS操作将状态为更新成功则代表当前线程获取锁,因此,将当前线程设置到AQS的一个变量中,说明这个线程拿走了锁。
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
             //如果不为0 意味着,锁已经被拿走了,但是,因为ReentrantLock是重入锁,
             //是可以重复lock,unlock的,只要成对出现行。一次。这里还要再判断一次 获取锁的线程是不是当前请求锁的线程。
                int nextc = c + acquires;//如果是的,累加在state字段上就可以了。
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

到此,如果如果获取锁,tryAcquire返回true,反之,返回false,回到AQS的acquire方法。

如果没有获取到锁,按照我们的描述,应该将当前线程放到队列中去,只不过,在放之前,需要做些包装。

先看addWaiter方法:

用当前线程去构造一个Node对象,mode是一个表示Node类型的字段,仅仅表示这个节点是独占的,还是共享的,或者说,AQS的这个队列中,哪些节点是独占的,哪些是共享的。

这里lock调用的是AQS独占的API,当然,可以写死是独占状态的节点。

创建好节点后,将节点加入到队列尾部,此处,在队列不为空的时候,先尝试通过cas方式修改尾节点为最新的节点,如果修改失败,意味着有并发,这个时候才会进入enq中死循环,“自旋”方式修改。

将线程的节点接入到队里中后,当然还需要做一件事:将当前线程挂起!这个事,由acquireQueued来做。

在解释acquireQueued之前,我们需要先看下AQS中队列的内存结构,我们知道,队列由Node类型的节点组成,其中至少有两个变量,一个封装线程,一个封装节点类型。

而实际上,它的内存结构是这样的(第一次节点插入时,第一个节点是一个空节点,代表有一个线程已经获取锁,事实上,队列的第一个节点就是代表持有锁的节点):

黄色节点为队列默认的头节点,每次有线程竞争失败,进入队列后其实都是插入到队列的尾节点(tail后面)后面。这个从enq方法可以看出来,上文中有提到enq方法为将节点插入队列的方法:

再回来看看

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
             //如果当前的节点是head说明他是队列中第一个“有效的”节点,因此尝试获取,上文中有提到这个类是交给子类去扩展的。
                    setHead(node);//成功后,将上图中的黄色节点移除,Node1变成头节点。
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && 
                //否则,检查前一个节点的状态为,看当前获取锁失败的线程是否需要挂起。
                    parkAndCheckInterrupt()) 
               //如果需要,借助JUC包下的LockSopport类的静态方法Park挂起当前线程。知道被唤醒。
                    interrupted = true;
            }
        } finally {
            if (failed) //如果有异常
                cancelAcquire(node);// 取消请求,对应到队列操作,就是将当前节点从队列中移除。
        }
    }

这块代码有几点需要说明:

1. Node节点中,除了存储当前线程,节点类型,队列中前后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为什么需要这个状态呢?

原因是:AQS的队列中,在有并发时,肯定会存取一定数量的节点,每个节点[G4] 代表了一个线程的状态,有的线程可能“等不及”获取锁了,需要放弃竞争,退出队列,有的线程在等待一些条件满足,满足后才恢复执行(这里的描述很像某个J.U.C包下的工具类,ReentrankLock的Condition,事实上,Condition同样也是AQS的子类)等等,总之,各个线程有各个线程的状态,但总需要一个变量来描述它,这个变量就叫waitStatus,它有四种状态:

分别表示:

  1. 节点取消
  2. 节点等待触发
  3. 节点等待条件
  4. 节点状态需要向后传播。

只有当前节点的前一个节点为SIGNAL时,才能当前节点才能被挂起。

2.  对线程的挂起及唤醒操作是通过使用UNSAFE类调用JNI方法实现的。当然,还提供了挂起指定时间后唤醒的API,在后面我们会讲到。

到此为止,一个线程对于锁的一次竞争才告于段落,结果有两种,要么成功获取到锁(不用进入到AQS队列中),要么,获取失败,被挂起,等待下次唤醒后继续循环尝试获取锁,值得注意的是,AQS的队列为FIFO队列,所以,每次被CPU假唤醒,且当前线程不是出在头节点的位置,也是会被挂起的。AQS通过这样的方式,实现了竞争的排队策略。

看完了获取锁,在看看释放锁,具体看代码之前,我们可以先继续猜下,释放操作需要做哪些事情:

  1. 因为获取锁的线程的节点,此时在AQS的头节点位置,所以,可能需要将头节点移除。
  2. 而应该是直接释放锁,然后找到AQS的头节点,通知它可以来竞争锁了。

是不是这样呢?我们继续来看下,同样我们用ReentrantLock的FairSync来说明:

unlock方法调用了AQS的release方法,同样传入了参数1,和获取锁的相应对应,获取一个锁,标示为+1,释放一个锁,标志位-1。

同样,release为空方法,子类自己实现逻辑:

protected final boolean tryRelease(int releases) {
            int c = getState() - releases; 
            if (Thread.currentThread() != getExclusiveOwnerThread()) //如果释放的线程和获取锁的线程不是同一个,抛出非法监视器状态异常。
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//因为是重入的关系,不是每次释放锁c都等于0,直到最后一次释放锁时,才通知AQS不需要再记录哪个线程正在获取锁。
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

释放锁,成功后,找到AQS的头节点,并唤醒它即可:

值得注意的是,寻找的顺序是从队列尾部开始往前去找的最前面的一个waitStatus小于0的节点。

到此,ReentrantLock的lock和unlock方法已经基本解析完毕了,唯独还剩下一个非公平锁NonfairSync没说,其实,它和公平锁的唯一区别就是获取锁的方式不同,一个是按前后顺序一次获取锁,一个是抢占式的获取锁,那ReentrantLock是怎么实现的呢?再看两段代码:

非公平锁的lock方法的处理方式是: 在lock的时候先直接cas修改一次state变量(尝试获取锁),成功就返回,不成功再排队,从而达到不排队直接抢占的目的。

而对于公平锁:则是老老实实的开始就走AQS的流程排队获取锁。如果前面有人调用过其lock方法,则排在队列中前面,也就更有机会更早的获取锁,从而达到“公平”的目的。

总结

这篇文章,我们从ReentrantLock出发,完整的分析了AQS独占功能的API及内部实现,总的来说,思路其实并不复杂,还是使用的标志位+队列的方式,记录获取锁、竞争锁、释放锁等一系列锁的状态,或许用更准确一点的描述的话,应该是使用的标志位+队列的方式,记录锁、竞争、释放等一系列独占的状态,因为站在AQS的层面state可以表示锁,也可以表示其他状态,它并不关心它的子类把它变成一个什么工具类,而只是提供了一套维护一个独占状态。甚至,最准确的是AQS只是维护了一个状态,因为,别忘了,它还有一套共享状态的API,所以,AQS只是维护一个状态,一个控制各个线程何时可以访问的状态,它只对状态负责,而这个状态表示什么含义,由子类自己去定义。


感谢郭蕾对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JDK1.8 AbstractQueuedSynchronizer的实现分析(上) 的相关文章

  • 使用 Java 的 Apache Http 摘要身份验证

    我目前正在开发一个 Java 项目 但无法使 http 摘要身份验证正常工作 我尝试使用 Apache 网站 但没有帮助 我有一个需要 HTTP 摘要身份验证的网站 DefaultHttpClient httpclient new Defa
  • 带路径压缩算法的加权 Quick-Union

    有一种 带路径压缩的加权快速联合 算法 代码 public class WeightedQU private int id private int iz public WeightedQU int N id new int N iz new
  • Java:扩展类并实现具有相同方法的接口

    可能无法完成以下操作 我收到编译错误 继承的方法 A doSomthing int 无法隐藏 B 中的公共抽象方法 public class A int doSomthing int x return x public interface
  • 有没有创建 Cron 表达式的 Java 代码? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我需要一个 Java 代码来根据用户输入创建一个 cron 表达式 用户输入是时间 频率和执行次数 只需从评论中添加 自己创建 即可
  • 两个整数乘积的模

    我必须找到c c a b mod m a b c m 是 32 位整数 但 a b 可以超过 32 位 我正在尝试找出一种计算 c 的方法 而不使用 long 或任何 gt 32 位的数据类型 有任何想法吗 如果m是质数 事情可以简化吗 注
  • 垃圾收集器如何在幕后工作来收集死对象?

    我正在阅读有关垃圾收集的内容 众所周知 垃圾收集会收集死亡对象并回收内存 我的问题是 Collector 如何知道任何对象已死亡 它使用什么数据结构来跟踪活动对象 我正在研究这个问题 我发现GC实际上会跟踪活动对象 并标记它们 每个未标记的
  • Android:文本淡入和淡出

    我已阅读此 stackoverflow 问题和答案 并尝试实现文本淡入和淡出 Android中如何让文字淡入淡出 https stackoverflow com questions 8627211 how to make text fade
  • Android中如何使用JNI获取设备ID?

    我想从 c 获取 IMEIJNI 我使用下面的代码 但是遇到了未能获取的错误cls 它总是返回NULL 我检查了环境和上下文 它们都没有问题 为什么我不能得到Context班级 我在网上搜索了一下 有人说我们应该使用java lang Ob
  • Jframe 内有 2 个 Jdialogs 的 setModal 问题

    当我设置第一个选项时 我遇到了问题JDialog模态 第二个非模态 这是我正在尝试实现的功能 单击 测试对话框 按钮 一个JDialog有名字自定义对话框 主要的将会打开 如果单击 是 选项自定义对话框主 其他JDialog named 自
  • Android studio - 如何保存先前活动中选择的数据

    这是我的代码片段 这Textview充当按钮并具有Onclicklistner在他们 当cpu1000时Textview单击它会导致cpu g1000其代码如下所示的类 public class Game 1000 extends AppC
  • 如何检查某个元素是否存在于一组项目中?

    In an ifJava中的语句如何检查一个对象是否存在于一组项目中 例如 在这种情况下 我需要验证水果是苹果 橙子还是香蕉 if fruitname in APPLE ORANGES GRAPES Do something 这是一件非常微
  • 如何在 Spring 中使 @PropertyResource 优先于任何其他 application.properties ?

    我正在尝试在类路径之外添加外部配置属性资源 它应该覆盖任何现有的属性 但以下方法不起作用 SpringBootApplication PropertySource d app properties public class MyClass
  • 从jar中获取资源

    我有包含文件的 jar myJar res endingRule txt myJar wordcalculator merger Marge class 在 Marge java 中我有代码 private static final Str
  • 在Java中运行bat文件并等待

    您可能会认为从 Java 启动 bat 文件是一项简单的任务 但事实并非如此 我有一个 bat 文件 它对从文本文件读取的值循环执行一些 sql 命令 它或多或少是这样的 FOR F x in CD listOfThings txt do
  • 如何将 HTML 链接放入电子邮件正文中?

    我有一个可以发送邮件的应用程序 用 Java 实现 我想在邮件中放置一个 HTML 链接 但该链接显示为普通字母 而不是 HTML 链接 我怎样才能将 HTML 链接放入字符串中 我需要特殊字符吗 太感谢了 Update 大家好你们好 感谢
  • 如何在flutter项目中使用http拦截器?

    我必须向我的所有 Api 添加标头 有人告诉我为此使用 http 拦截器 但我无法理解如何做到这一点 因为我是颤振的新手 谁能帮我举个例子吗 您可以使用http 拦截器 https pub dev packages http interce
  • 为什么\0在java中不同系统中打印不同的输出

    下面的代码在不同的系统中打印不同的输出 String s hello vsrd replace 0 System out println s 当我在我的系统中尝试时 Linux Ubuntu Netbeans 7 1 它打印 When I
  • 部署 .war 时出现 Glassfish 服务器错误:部署期间发生错误:准备应用程序时出现异常:资源无效

    我正在使用以下内容 NetBeans IDE 7 3 内部版本 201306052037 爪哇 1 7 0 17 Java HotSpot TM 64 位服务器虚拟机 23 7 b01 NetBeans 集成 GlassFish Serve
  • 手动设置Android Studio的JDK路径

    如何为 Android Studio 使用自定义 JDK 路径 我不想弄乱 PATH 因为我没有管理员权限 是否有某个配置设置文件允许我进行设置 如果您查看项目设置 您可以从那里访问 jdk 在标准 Windows 键盘映射上 您可以在项目
  • 由 Servlet 容器提供服务的 WebSocket

    上周我研究了 WebSockets 并对如何使用 Java Servlet API 实现服务器端进行了一些思考 我没有花费太多时间 但在使用 Tomcat 进行一些测试时遇到了以下问题 如果不修补容器或至少对 HttpServletResp

随机推荐

  • 四种解决”Argument list too long”参数列表过长的办法

    四种解决 Argument list too long 参数列表过长的办法 转自 http hi baidu com cpuramdisk item 5aa49ce00c0757aecf2d4f24 在linux中删除大量文件时 直接用rm
  • 调试web项目时Chrome浏览器发送两次请求

    最近调试web项目时 项目有时候会因为接收到空值而报错 之后我发现是因为Chrome浏览器会连续发送2次请求导致 在使用Edge浏览器则没有出现这个问题 遂搜索了一些解决方案如下 https blog csdn net weixin 390
  • Relational Knowledge Distillation解读

    Relational Knowledge Distillation解读 Relational Knowledge Distillation Title Summary Research Objective Problem Statement
  • 图形学相关期刊和会议的基本信息

    目录 期刊 A类 ACM TOG A类 IEEE TIP A类 IEEE TVCG B类 TOMCCAP B类 CAGD B类 CGF B类 CAD B类 GM B类 TCSVT B类 TMM B类 SIIMS C类 CGTA C类 CAV
  • GDB -- 多线程堆栈

    1 死机后 输入 info threads 查看所有thread信息 2 thread apply all bt 显示所有的线程堆栈 示例 gdb info threads Id Target Id Frame 3 Thread 0xb77
  • html写了外部样式表,外部样式表怎么写

    1 css内部样式表怎么写 1 创建使用css样式表有三种 分别是外部样式表 内部样式表和内联样式 下面通过一个小demo演示它们的用法 首先新建一个html文件 放入3个button按钮 给前两个按钮分百别设置class属性为btn1和b
  • spring中的设计模式

    转自 http ylsun1113 iteye com blog 828542 我对设计模式的理解 应该说设计模式是我们在写代码时候的一种被承认的较好的模式 就像一种宗教信仰一样 大多数人承认的时候 你就要跟随 如果你想当一个社会存在的话
  • 11. Container With Most Water

    Given n non negative integers a1 a2 an where each represents a point at coordinate i ai n vertical lines are drawn such
  • ESP32C3解锁使用IO11

    目录 1 使用pip安装esptool 2 安装idf开发命令行环境 可参考 3 将开发板插入电脑 4 打开IDF CMD命令行 5 打开命令行窗口 源自官方wiki 本篇介绍如何给ESP32C3多释放一个io ESP32C3的GPIO11
  • 如何从JavaScript数组中获取多个随机唯一元素?

    The JavaScript is a very versatile language and it has a function almost everything that you want JavaScript是一种非常通用的语言 它
  • Everything使用攻略和技巧

    Everything使用技巧 www hi channel com出品本文为H4海畅智慧原创文章 未经允许不得进行商业盈利性转载 非盈利性商业转载请注明出处www hi channel com 1 Everything下载地址 http w
  • access和tagware_通信缩略语

    英文缩写 英文名称 中文名称 3G The third generation mobile communications 第 3 代 移动通信 3GPP2 3rd Generation Partnership Project 2 3G 协作
  • 在论文开题报告中,研究目的和研究意义两者之间有什么区别吗?

    相信很多同学在接触论文的时候 会分不清研究目的和研究意义两者之间有什么区别 别着急 通过对大量文献的分析并根据数位研究生导师的讲解 这里总结出一篇针对二者区别的详细解读 全文大约有2000字 利用理论和实例全方位为大家解惑 选题的目的和意义
  • 【Spring Boot 集成应用】Spring Boot Admin的集成配置使用

    1 Spring Boot Admin 简介 Spring Boot Admin是一个开源社区项目 用于管理和监控SpringBoot应用程序 每个应用都认为是一个客户端 通过 HTTP 或者使用 Eureka 注册到 admin serv
  • 数字图像处理第十一章

    表示和描述 由于本章注重于如何存储 以后学习过程中多半不会用到该章节的知识 因此本章只做大概介绍 不再使用代码进一步说明 将一幅图像分割成多个区域后 分割后的像素集需要以一种合适于计算机进一步处理的形式来表示和描述 表示 表示一个区域的两种
  • sql2008计算机环境,win2008r2下安装sql2008r2初版

    步骤一 安装前的准备 软件要求 1 SQL Server 安装程序安装该产品所需的以下软件组件 NET Framework 3 5 SP11 SQL Server Native Client SQL Server 安装程序支持文件 2 所有
  • 洗牌牛客网

    链接 https www nowcoder com questionTerminal 5a0a2c7e431e4fbbbb1ff32ac6e8dfa0 来源 牛客网 洗牌在生活中十分常见 现在需要写一个程序模拟洗牌的过程 现在需要洗2n张牌
  • Matlab——回归分析

    基础知识 函数ones a b 产生a行b列全1数组 ones a 产生a行a列全1数组 zeros 同理 Y y Y为y的转置矩阵 函数size 获取数组的行数和列数 1 s size A 当只有一个输出参数时 返回一个行向量 该行向量的
  • MG995舵机控制

    左右按键 单次旋转15度 锁相环不分频 倍频 只是为了锁定频率 KEY M键旋转到中间位置 舵机的控制脉冲是0 5ms 2 5ms 1 5ms时居中 但是会存在一定的偏差 1 2 Module MG995 3 Author YangFei
  • JDK1.8 AbstractQueuedSynchronizer的实现分析(上)

    深度解析Java 8 JDK1 8 AbstractQueuedSynchronizer的实现分析 上 作者 刘锟洋 发布于 2014年7月31日 http www infoq com cn articles jdk1 8 abstract