写点什么

剖析 java.util.concurrent 锁

2010 年 9 月 10 日

来自 IBM 的工程师齐尧,Raja Das 和罗志达在本文中介绍了 jucprofiler,这个 alphaWorks 上的工具用于剖析使用了 java.util.concurrent 类的多 核平台上的应用程序,而 java.util.concurrent 类则是在 Java 5 中引入的。

1. 导言

程序的性能分析是应用程序开发过程中的一个重要方面。这个工作一般是由一些专业人员来完成的,他们的目标是在一个特定的平台上,提高代码的性能。当程序是运行在多核平台的多线程或者并行程序的时候,提高性能这个问题就变得更加困难了。因为在这样的情况下,不仅需要考虑代码的性能,还需要考虑代码的可伸缩性。

随着 Java 5 中引入了 java.util.concurrent (JUC) 包,在 Java 语言中出现了一种新的锁。JUC 包使用得越来越普遍,因为更多的应用程序需要为了多核系统而开发或仔细地调优。虽然 JLM 可以找到传统的 Java 锁的详细的竞争信息,但是却没有同样的工具能够找到 java.util.concurrent.locks 包的锁竞争信息。Sun/Oracle、IBM,还有其他 Java 厂商也都没有这样的工具。缺乏对 JUC 锁的剖析工具正是我们开发这个锁工具,jucprofiler( Multicore SDK 的一部分)的动机。

2. jucprofiler 概览

当在程序中使用 JUC 锁的时候,线程会在下面两种情况下“停止”执行:

  1. 当线程 A 试图去获得一个 JUC 锁,但这个锁却已经被另外一个线程获得,那么线程 A 不得不“停止”,直到这个锁被释放或者超时。
  2. 当线程 A 调用了 java.util.concurrent.locks.Condition 的任意一个“等待”的 API,线程 A 会停止执行,直到另外一个线程通知它或者超时。

我们分别把这两种情况称作“锁竞争时间”和“等待时间”。

jucprofiler 就是为了捕获以上两种情况的时间开销而设计和实现的。

2.1. 代码修改(Instruments)

为了获取 JUC 锁的运行时数据,需要提前修改一些 JUC 类,然后替换掉 JRE 中相应的类。在首次使用 jucprofiler 之前,用户需要运行命令去生成 PreInstrument.jar。假设 JRE 没有改变的话,这个步骤只需要做一次。(如果用户改变了 JRE,那么用户需要自己删除 PreInstrument.jar,然后重新运行这个命令,来再次生成 PreInstrument.jar)。

2.1.1. 锁竞争时间开销

对于锁竞争时间开销,jucprofiler 记录了申请类 java.util.concurrent.locks.AbstractQueuedSynchronizer 和类 java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject 的实例,并且给这些实例分配唯一的标识。

方法

调用位置

java.util.concurrent.locks.LockSupport

park (Object);

类 AbstractQueuedSynchronizer 中的方法 parkAndCheckInterrupt()

复制代码
parkNanos(Object blocker, long nanos)

类 AbstractQueuedSynchronizer 中的方法 doAcquireNanos(int arg, long nanosTimeout) 与 doAcquireSharedNanos(int arg, long nanosTimeout)

2.1.2. 锁等待时间开销

对于锁等待时间开销,jucprofiler 获取了在不同的位置调用类 java.util.concurrent.locks.LockSupport 的方法 park(blocker) 与 parkNanos(blocker, nanos) 的时间开销:

方法

调用位置

java.util.concurrent.locks.LockSupport

park (Object);

类 AbstractQueuedSynchronizer 除 parkAndCheckInterrupt() 以外的方法

复制代码
parkNanos(Object blocker, long nanos)

类 AbstractQueuedSynchronizer 除 doAcquireNanos(int arg, long nanosTimeout) 与 doAcquireSharedNanos(int arg, long nanosTimeout) 以外的方法

3. 如何使用

本节将通过一个真实的应用来探究如何使用 jucprofiler 来寻找程序中的问题。

3.1. 运行 jucprofiler

使用如下的命令:

复制代码
$ java -Xbootclasspath/p:$JUCP/BCIRuntime.jar:$JUCP/PreInstrument.jar
-javaagent:$JUCP/BCIAgent.jar=callStackDepth=10:allocationStackDepth=0
:,msdk.idp=com.ibm.msdk.bciagent.JUCInstrumentDecisionProvider,:libPath=$JUCP:traceJUC=on
-cp .:derby.jar JavaDBDemo

jucprofiler 可以用于任何运行在 JDK6 上的 Java 程序上。假设 jucprofiler 安装在了目录 $JUCP 中,在 jucprofiler 运行完你的程序之后,就会生成一个叫"BCIAgent.*.bci.trace"的文件,其中"*"代表本次运行的唯一时间戳。

3.2. 获取结果

运行如下所示的命令来得到结果:

复制代码
$ java -Xmx1000m -jar $JUCP/BCITraceReader.jar {tracefile} {resultOutputFile}

其中:

  1. {tracefile}是一个追踪文件的全路径名称或者是一个包含若干追踪文件的目录名称,比如 BCIAgent.***.bci.trace。
  2. {resultOutputFile}是可选的参数,用来指定存放分析结果的文件。如果没有这个选现,那么分析结果就会在控制台打印出来。

说明:因为对追踪文件事后分析过程会有一些内存开销,所以最好用 Java 选项 -Xmx 来增加堆的大小。在我们的实验里,分析 160M 的文件,需要 800M 的内存。

3.3. 结果解析

如下图所示,纯文本输出包含了不同类型的信息,比如锁的名称,锁竞争的次数和时间,锁被持有的时间和次数,锁在申请时线程的调用栈,持续的时间和每一次锁竞争的调用栈。这些结果有助于用户发现 JUC 锁竞争造成的程序瓶颈。

在“LEGEND” 段之前,结果报告首先,按照锁竞争次数和时间的降序,总结了程序中全部的 JUC 锁竞争。其中每一行属于两种类型的一种,“AQS”代表 JUC 锁,“CHM”代表 ConcurrentHashMap. 因为一个 ConcurrentHashMap 内部被分割为了若干个片段(segment)进行存储,而且每一个片段都被一个不同的 JUC 锁保护,所以,从锁的角度来看,ConcurrentHashMap 可以被看作为一个 JUC 锁的集合。例如,“CHM@8”有 276 次锁竞争,一共 3,945,7000 纳秒的竞争时间,这就是说,“CHM@8”中的所有片段的 JUC 锁共有 276 次锁竞争,一共 3,945,7000 纳秒的竞争时间。这样的对锁的分组能够帮助程序员发现哪一个 ConcurrentHashMap 对象发生了最严重的锁竞争。相反,JUC 锁“AQS@1790” 不属于任何一个 ConcurrentHashMap 对象,它就是程序中一个显式使用的锁。

说明:因为在这个例子中,没有打开记录获得锁的功能,所以列 HOLD-COUNT 和 HOLD-TIME 都是零。

在“LEGEND”段之后,结果给出了每一个 JUC 锁竞争的详细信息。在下边的结果片段中,对于 ConcurrentHashMap “CHM@8” ,锁竞争出现在两个用来保护片段的锁上 “Lock [AQS@135]” and “Lock [AQS@146]”。对于“Lock [AQS@135]”,它在一个程序位置出现竞争,显示了竞争的次数,时间和竞争时刻的线程调用栈。对于“Lock [AQS@146]”,给出了同样的信息。这些细节信息可以很好地帮助程序员来定位程序中的锁竞争,并且清楚地理解 ConcurrentHashMap 中哪个片段竞争最严重。

复制代码
Multicore Software Development Toolkit Version_2.1
j.u.c Lock Profiler Report
NAME CONTD-COUNT CONTD-TIME HOLD-COUNT HOLD-TIME
CHM@8 276 39457000 0 0
AQS@1790 36 4029000 0 0
AQS@131 17 630000 0 0
=================================================================================================
LEGEND:
NAME : Name of juc lock(AQS) or ConcurrentHashMap(CHM), format: <Type>@<Idgt;
CONTD-COUNT : Total count of lock contention
CONTD-TIME : Total time of lock contention in nanosecond
HOLD-COUNT : Total count of lock hold
HOLD-TIME : Total time of lock hold in nanosecond
==================================================================================================
ConcurrentHashMap [CHM@8]:
-----------------------------------------------------------------------------------------------------------
Lock [AQS@135]:
-----------------------------------------------------------------------------------------------------------
Lock Contention 1
CONTD-COUNT: 25
CONTD-TIME: 10827000
Call Stack:
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:758)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:789)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1125)
java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:197)
java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:273)
java.util.concurrent.ConcurrentHashMap$Segment.remove(ConcurrentHashMap.java:530)
java.util.concurrent.ConcurrentHashMap.remove(ConcurrentHashMap.java:934)
org.apache.derby.impl.services.locks.ConcurrentLockSet.unlock(ConcurrentLockSet.java:740)
org.apache.derby.impl.services.locks.ConcurrentLockSet.unlockReference(ConcurrentLockSet.java:784)
org.apache.derby.impl.services.locks.LockSpace.unlockReference(LockSpace.java:275)
Lock [AQS@146]:
-----------------------------------------------------------------------------------------------------------
Lock Contention 1
CONTD-COUNT: 22
CONTD-TIME: 2009000
Call Stack:
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:758)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:789)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1125)
java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:197)
java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:273)
java.util.concurrent.ConcurrentHashMap$Segment.remove(ConcurrentHashMap.java:530)
java.util.concurrent.ConcurrentHashMap.remove(ConcurrentHashMap.java:934)
org.apache.derby.impl.services.locks.ConcurrentLockSet.unlock(ConcurrentLockSet.java:740)
org.apache.derby.impl.services.locks.ConcurrentLockSet.unlockReference(ConcurrentLockSet.java:784)
org.apache.derby.impl.services.locks.LockSpace.unlockReference(LockSpace.java:275)

3.4. 在可视化分析器中打开追踪文件

可视化分析器,作为 multicore SDK 的一部分,提供了一些 Eclipse 视图,可以用表格和图表来显示 jucprofiler 文件。当前有两个视图,一个叫“J.U.C 统计”视图,另外一个叫“J.U.C 同步”视图。

“J.U.C 统计”视图如下所示。右边的两列是“竞争时间”和“竞争次数”。“申请栈”是关于 JUC 锁在申请的时候的调用位置。

“J.U.C 同步”视图如下所示。第一列是时间,表明什么时候锁出现了竞争。第二列是线程,表明锁竞争发生哪个线程。第三列是锁,表明那个 JUC 锁出现竞争了。最后一列是方法,表明锁竞争是在什么位置发生的。

3.5. 在线控制

在执行的过程中,jucprofiler 会创建一个“控制服务器”监听 2009 端口。用户可以使用“控制客户端”连接这个端口,控制 jucprofiler 的行为,比如,可以动态打开或者关闭追踪功能:

复制代码
$ java -cp BCIRuntime.jar
复制代码
com.ibm.msdk.bciruntime.control.ControlClient HOST -m [b|i]b
复制代码
START -e END

其中:

HOST:“控制客户端”要去连接的主机名称,缺省是本机。

-m [b|i]: “控制客户端”执行的模式。- b 是批处理模式、- i 是交互模式。缺省是交互模式。

-b START:如果是批处理模式,START 是开始剖析的时间。

-e END:END 是剖析过程的持续时间。

3.5.1. 交互模式

有一个简单的 shell,用户可以输入一些命令 juc.on 和 juc.off 来打开和关闭 jucprofiler。比如,java -cp BCIRuntime.jar com.ibm.msdk.bciruntime.control.ControlClient, “控制客户端” 会连接到本机, 并且打开一个 shell 来控制 jucprofiler.

复制代码
$ java -cp BCIRuntime.jar com.ibm.msdk.bciruntime.control.ControlClient
jucprofiler control> juc.on
juc.on
jucprofiler t control> start
start
jucprofiler control> stop
stop
jucprofiler control> juc.off
juc.off
jmit control> quit
quit
复制代码
$ java -cp BCIRuntime.jar com.ibm.msdk.bciruntime.control.ControlClient localhost -m b -b 2 -e 10
Start tracing in 2 seconds
Start tracing
Stop tracing in 10 seconds
Stop tracing
quit

3.5.2. 批处理模式

jucprofiler 也支持批处理模式。比如, java -cp BCIRuntime.jar com.ibm.msdk.bciruntime.control.ControlClient mtrat-test.dyndns.org -m b -b 10 -e 10, 意思是 “控制客户端”会连接到机器 mtrat-test.dyndns.org,10 秒后启动 jucprofiler,然后 10 秒后停止 jucprofiler。

4. 结论

随着多核处理器成为主流,更多的并行 / 多线程 Java 程序将不断涌现。我们需要更好的工具去剖析这些并行程序。本文介绍的 jucprofiler 填补了 Java 性能分析工具中的一个重要缺口。

关于作者

齐尧是 IBM 中国开发中心新技术中心的工程师。他于 2006 年加入 IBM。齐先生在 Java 虚拟机,字节码修改和 Java 编程方面都很有经验。他现在从事的项目是 Java 的运行时分析工具。齐先生在北京理工大学获得了计算机科学的硕士学位。

Raja Das 是 IBM 软件部的软件架构师。他现在为多核系统开发库和框架。之前,他是 WebSphere?? Partner Gateway 的产品架构师。Das 先生的兴趣包括编程语言,并行软件和系统。

罗志达是 IBM 中国开发中心新技术中心的工程师。他于 2008 年加入 IBM,在嵌入式软件开发上很有经验,现在的项目是运行时分析工具。罗先生在北京大学获得软件工程硕士学位。

查看英文原文: Profiling java.util.concurrent locks


感谢张龙对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010 年 9 月 10 日 00:0010079

评论

发布
暂无评论
  • 使用 Java 构建高伸缩性组件

    本文介绍了开发高伸缩应用(利用底层多核硬件)的支持库,该库属于Amino库的一部分。其中一个例子是:通过java.util.concurrent.ConcurrentHashMap替代同步的HashTable来确保应用的扩展性。

  • Java 调试——回归基础

    目前可以用来调试Java应用程序的工具令人叹为观止。现代IDE提供了全功能的调试界面,并且像GlassBox这样的工具用BCI(bytecode instrumentation)提供了关于应用程序运行时状态的更多信息。虽然你有时候会发现自己处于无法使用这些工具的境地,或许应用程序被部署在防火墙的另一端,不允许你远程调试;或许你正在客户那里,没有你喜欢的工具可用。现代调试工具使用的大多数信息和方法都由JVM自己提供,并具有控制这种信息而不用最新图形工具的一些方法。本文关注一些可用的调试方法,它们只使用Java SDK(JDK)自带的那些工具。

  • 一文讲清 JVM 内存结构 | 极客视点

    一直都是程序员面试的必考点,但是要把它搞清楚又好像不是特别容易。的知识点太散,不系统,所以不便于归纳总结。

  • Java 6 中的线程优化真的有效么?——第二部分

    像偏向锁、锁粗化、通过逸出分析的锁省略以及自适应的自旋锁等技术,都是为了提高并发性而出现的。它们允许应用程序线程之间可以更多更高效地共享数据。但是它们真的有效么?在这篇由两部分组成的文章里,Jeroen Borgers将逐一探究这些特性,并尝试在单一线程基准的协助下,回答关于性能的问题。本篇文章是这个系列的第二部。

  • 用上 ConcurrentHashMap,就没有并发问题了?

    这篇算是《极客时间》王宝令老师专栏《Java并发编程实战》的课后笔记吧。 从一个用户注册的例子开始,分析ConcurrentHashMap使用注意点

  • Java 6 中的线程优化真的有效么?

    像偏向锁、锁粗化、通过逸出分析的锁省略以及自适应的自旋锁等技术,都是为了提高并发性而出现的。它们允许应用程序线程之间可以更多更高效地共享数据。但是它们真的有效么?在这篇由两部分组成的文章里,Jeroen Borgers将逐一探究这些特性,并尝试在单一线程基准的协助下,回答关于性能的问题。

  • 预览“Java 数码录像机”—— Chronon Recording Server

    Chronon System近期公布了Chronon Recording Server的beta版本,它是Chronon工具箱系列产品中一款远程控制工具,可以记录Java应用运行时的内部状况。之后,工程师可以通过其中的“回放”功能检查或调试整个程序。Chronon Systems把它称作是“Java的数码录像机”。

  • Java 深度历险(三)——Java 线程​:基本概念、可见性与同步

    对于Java来说,在语言内部提供了线程的支持。但是Java的多线程应用开发会遇到很多问题。首先是很难编写正确,其次是很难测试是否正确,最后是出现问题时很难调试。一个多线程应用可能运行了好几天都没问题,然后突然就出现了问题,之后却又无法再次重现出来。如果在正确性之外,还需要考虑应用的吞吐量和性能优化的话,就会更加复杂。本文主要介绍Java中的线程的基本概念、可见性和线程同步相关的内容。

  • Java 虚拟机的监控及诊断工具(GUI 篇)

    eclipse MAT可用于分析由jmap命令导出的Java堆快照。Java Mission Control是Java虚拟机平台上的性能监控工具。

    2018 年 10 月 1 日

  • 深入理解 Java 内存模型(五)——锁

    Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,本文试图揭开Java内存模型神秘的面纱。本文大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)的内存语义,重排序规则及在处理器中的实现;Java内存模型的设计目标,及其与处理器内存模型和顺序一致性内存模型的关系。

  • JDK 和 JRE

    2019 年 7 月 22 日

  • 程序员练级攻略:Java 底层知识

    Java最黑科技的玩法是字节码编程,从而使得Java这门静态语言在运行时可以进行各种动态的代码修改。

    2018 年 7 月 3 日

  • 垃圾回收(上)

    Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。它从一系列GC Roots出发,边标记边探索所有被引用的对象。

    2018 年 8 月 15 日

  • 5 道常见的 Java 面试题

    前Oracle首席工程师为你讲解Java常见面试题,带你巩固Java核心知识点。

  • Tomcat 内存溢出的原因分析及调优

    作为Java程序员,我们几乎都会碰到OutOfMemoryError异常,你知道有哪些原因可能导致JVM抛出这个异常吗?

    2019 年 8 月 6 日

  • Java 深度历险(一)——Java 字节代码的操纵

    Java类文件中包含的字节代码可以被不同平台上的JVM所使用。Java字节代码不仅可以以文件形式存在于磁盘上,也可以通过网络方式来下载,还可以只存在于内存中。JVM中的类加载器会负责从包含字节代码的字节数组(byte[])中定义出Java类。在某些情况下,可能会需要动态的生成 Java字节代码,或是对已有的Java字节代码进行修改。这个时候就需要用到本文中将要介绍的相关技术。

  • 第 12 讲 | Java 有几种文件拷贝方式?哪一种最高效?

    我在上一讲提到,NIO不止是多路复用,NIO 2也不只是异步IO,今天我们来看看Java IO体系中其他不可忽略的部分。

    2018 年 5 月 31 日

发现更多内容

架构师训练营第 1 期 -- 第三周作业

发酵的死神

极客大学架构师训练营

架构师训练营第 1 期 -- 第三周学习总结

发酵的死神

极客大学架构师训练营

Golang单例模式手写稿

Jacky.Chen

架构师训练营 Week3 代码重构 - 学习总结 设计模式

spring 设计模式 JUnit

架构师训练营第 1 期 - 第三周学习总结

Anyou Liu

极客大学架构师训练营

架構師訓練營第 1 期 - 第 02 周作業

Panda

架構師訓練營第 1 期

JD-GUI反编译jar包为Java源代码

MySQL从删库到跑路

Java jar 程序员 Spring Boot jar包的小秘密

第三周作业

极客大学架构师训练营

架构师训练营第 1 期 week2 总结

张建亮

极客大学架构师训练营

spring-boot-route(二)读取配置文件的几种方式

Java旅途

Java Spring Boot

分布式系统的核心:共识问题

多颗糖

分布式计算 计算机基础 分布式系统 架构师

第三周总结

_

极客大学架构师训练营 第三周总结

Linux忘记root密码怎么办

MySQL从删库到跑路

Linux 服务器 root密码 root

week03

……

Mongodb异常关闭,再次启动报错

MySQL从删库到跑路

mongodb

架构师训练营第二周总结

xs-geek

架构师训练营第三周学习总结

成长者

极客大学架构师训练营

架构师训练营第 1 期 week2

张建亮

极客大学架构师训练营

Springboot 邮件任务

hepingfly

springboot 发送邮件

为什么Rust的println!不会发生所有权转移?

袁承兴

rust 元编程

架构师训练营第三周命题作业

成长者

极客大学架构师训练营

LeetCode题解:242. 有效的字母异位词,数组计数,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

设计模式第三周作业「架构师训练营第 1 期」

天天向善

单例模式 组合模式

第三周作业

icydolphin

极客大学架构师训练营

设计模式第三周总结「架构师训练营第 1 期」

天天向善

Week 3 Assignment

Yinan

最完整的PyTorch数据科学家指南(2)

计算机与AI

学习 PyTorch

架構師訓練營第 1 期 - 第 02 周總結

Panda

架構師訓練營第 1 期

集中日志系统ELK

青乡之b

ELK

架构师训练营第二周作业

xs-geek

vagrant 开发环境配置

孙志平

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

剖析java.util.concurrent锁-InfoQ