Java性能问题排查经验分享

前言

好久没动笔写博客了,为了不给2017留空白,特写此篇。 这篇文章提到的案例、方法、方案有些是在之前的文章中会零散提到过,都是在公司内部分享多次以及迭代多次的结果,当然,敏感数据的地方我会换成一些fake的数据,注重方法论,数据上有问题的地方请包容。

这个话题其实可以涉及到各种各样的场景,我这里仅仅是把我们公司内部经常遇到的一些问题拉出来分享。本文的案例基本上都基于linux环境的java程序进行分析,如果你是windows或者其他的操作系统,可能有些工具需要自己寻找、下载。

首先讲讲排查思路

1. 出现问题之前,是否刚刚做了发布(上线新的代码:功能、修改等)

  • 是:结合内部的服务器监控系统(zabbix等)的数据,判断是否在发布的时间节点前后发生不一样的现象,如果是,基本可以断定是新代码导致的问题,第一时间回滚代码(如果可以的话,技术方案设计上本来也要考虑失败回滚的问题),先恢复应用的正常访问(服务),再分析新上的代码是否有问题。
  • 否:如果是历史遗留的问题,就需要结合工具来排查(这写就是本文会讲解的重点)

2. 关于日志

  • 日志不是越详细越好,记录关键信息才是真理。如果每次打印日志都把过程数据写上,反而容易引起磁盘io问题、内存频繁ygc问题。
  • 日志要有意义,debug性质的日志一律用 debug级别,线上禁止打印 debug级别的日志。
  • 区分性能日志和业务日志:性能日志用于发现性能问题,业务日志用于记录业务流水方便后期追溯。

排查工具

  1. ps | grep 组合命令, 方便快速找到进程id(java有自带的jps工具)
  2. vi/tail/head/more/less: 查看线上日志的工具,注意不要用vim打开打日志,往往会给(内存)负担过重的服务器致命一击
  3. awk 文本分析脚本,可以快速分析性能、业务日志,得出结果,不用等大数据平台一系列流程
  4. top:查看进程(和线程)的cpu耗时,内存占用情况
  5. iotop:查看进程和线程的IO消耗情况 (需要自行安装)
  6. iftop:查看进程和线程的网络情况 (需要自行安装)
  7. lsof:查看文件打开情况(含网络、本地文件、设备等)
  8. JVM系列tools (本文重点)
    • 8.1 jstat:查看进程内存分配和gc情况
    • 8.2 jstack:查看进程的线程栈
    • 8.3 jmap:dump java 内存。(不要对在线提供服务的应用做jmap)
    • 8.4 btrace:运行中的代码调试
    • 8.5 jprofile: 外挂的形式,分析具体到方法级别的相应耗时,压测配合jprofile,能直接找到瓶颈
  9. IBM Memory Analyzer Tool, 简称MAT,用于分析 jmap 命令dump出来的内存文件
  10. 监控工具
    • 10.1 zabbix:观察系统的历史性能状态(顺便告警)
    • 10.2 ganglia:观察系统的历史性能状态
    • 10.3 vision: 我司内部自研系统,观察业务的性能数据,QPS vs RT
    • 10.4 各式APM工具,比如OneAPM,听云等,通过javaagent的方式实时分析程序的性能问题

一些案例

案例1. 进程耗CPU咋办

一般两种问题:
– a. 业务代码中有大(或死)循环,消耗大量CPU计算资源
– b. 垃圾回收(GC)频繁,导致gc线程消耗大量CPU (FullGC、YoungGC均会消耗CPU资源)

针对a情况,如何找到消耗CPU的代码段?

  • ps | grep 组合键找到进程id
  • top -H -p $pid 列出线程详情 (-H命令可以显示线程情况)
  • 找到耗CPU最高的几个线程ID ( 同时按下 shift + T , 按CPU时间(Time)排序) ,在-H模式下,pid就是线程id
  • printf %x $tid 把线程ID转化为16进制, 记录下来
  • jstack -l $pid ,dump出线程栈 ,最好写到文件里
  • 通过第四步得到的线程ID的16进制,在线程栈里面找到相应的线程栈信息 (nid=0x16进制线程ID)

至此,定位到的代码段一般就是耗CPU的代码段了。

通过上述排查,如果耗CPU的线程是VM Thread,说明是进入了b情况

  • 首先,我们必须清楚gc的情况,如果没有开启gc日志,只能通过 jstat命令查看
  • jstat -gc $pid , 观察 YoungGCCount 和 FullGCCount 的增速
  • 如果ygc快速增加,说明是新生代内存分配和回收过快 (比如每秒增加几十次ygc),此时需要结合观察日志,看某种业务(服务)是否在快速打日志,是的话一般就是此处的频繁工作引起问题。
  • 如果fgc快速增加,说明是老生代的内存一直不够用(晋升失败),此时可以通过jmap 命令dump内存,到本地用mat工具分析

案例2. 进程or线程hang住了怎么办

一般原因

  • 死锁(本地锁&分布式锁)
  • 依赖的远程服务hang住&没有设置超时时间
  • 线程池耗尽 or 连接池耗尽
  • 死循环
  • 调用网络服务,但网卡带宽被耗尽

解决方法

  1. 排查是否死锁
  2. 设置远程服务(含API、MQ消息、分布式锁等)的超时时间,业务需要兼容
  3. 调高线程池or连接池 & 缩短超时时间(业务需要做兼容)

排查死锁的方法

很简单,通过jstack工具,他会直接告诉你死锁在哪。
1. 找出进程ID
2. jstack -l dump出线程栈
3. 通过 deadlock 关键字找到相关线程信息即可

案例3. 进程周期性卡顿怎么办

一般原因

  • 有定时任务,定时任务触发时把系统拖慢
  • 有定时fullgc,可能是定时任务导致的fullgc,也可能是定时触发fullgc。fullgc时会stop the world。

解决方法

  • 如果是定时任务,优化定时任务对内存的使用
  • 如果是定时触发fullgc,而系统不能接受,则配置vm参数禁用fullgc

排查过程

  1. 启动参数中要添加gc日志
  2. 如果没有gc日志,则通过jstat -gc 命令来查看gc情况
  3. 如果OU(old区使用量)远远没有达到OC(old区容量)就触发了fullgc,一般是定时触发,可以通过添加 -XX:+DisableExplicitGC 参数禁用定时触发的fullgc。

案例4. 进程OOM怎么办

首先,要先知道有哪些OOM,每种OOM都是有什么问题引起的。

OOM种类

  • gc overhead limit exceeded :jvm花大量时间回收少量的内存。
  • java heap space:heap内存不够用,无法继续分配内存,且无法回收足够的内存
    • heap的大小由 -Xms 和 -Xmx 决定
    • 一般配置了 -XX:-UseGCOverheadLimit 就不会出现gc overhead limit exceeded 问题, 最终会变成 java heap space
  • unable to create new native thread:超过资源限制
    • 进程、线程数超过了系统限制(ulimit)
    • 线程数超过了kernel.pid_max的限制
  • perm gen space: 持久代不够用
    • 持久代不够用,调大 PermSize 参数
    • 调大也没用?怀疑classloader错误使用。
  • direct buffer memory :堆外内存使用超出限制的大小
    • 如果机器内存足够大,可以调大 -XX:MaxDirectMemorySize 参数
    • 一般是网络通信没有限流,而且用内存做buffer
    • 定时fgc 主动回收堆外内存
  • map failed: FileChannel map的文件超过了限制
    • 调大 vm.max_map_count 系统参数可解
  • Requested array size exceeds VM limit: 创建数组大小超过jvm限制
    • 创建Integer.MAX_VALUE – n 以上长度的数组会抛出, n和jvm实现、系统环境有关
  • request ? bytes form ?. Out of swap space
    • 地址空间不够用(一般32bit系统才会碰到),物理内存耗光
    • 强制触发fullgc看有没有好转,有的话可能是DirectByteBuffer误用造成的
    • jmap -histo:live $pid 可以强制触发fullgc。

排查过程

  1. 先把服务摘离线上服务集群
  2. dump服务的内存,由于OOM后java进场可能会无法直接访问,需要使用jmap的-F参数强制dump
  3. 将dump出来的文件拉到本地环境,用mat工具分析。

参考链接

  1. 阿里研究员 毕玄 的博客:http://bluedavy.me/
  2. 官方troubleshooting文档:http://dwz.cn/javatsg
  3. OOM shooting: https://plumbr.io/outofmemoryerror

shallow heap vs retained heap

在java内存分析软件(mat,jhat等)中,有两个概念是 shallow heapretained heap (有时候叫shallow size 和 retained size)。

shallow heap

比较好理解(好理解不代表好计算),直译就是浅层堆,其实就是这个对象实际占用的堆大小。

retained heap

比较难理解,直译过来是保留堆,一般会大于或者等于shallow heap,那么retained heap如何理解呢?

retained heap 的计算方法

首先,不能按照 shallow(浅) 和 deep(深)的层次来理解这个retained heap,其实最简单的理解就是,如果这个对象被删除了(GC回收掉),能节省出多少内存,这个值就是所谓的retained heap。而GC算法中,是否回收一个对象,主要是判断一个对象是否存在引用(还有一些系统级别或特定对象不在此列),至于标记还是引用计数算法,最终都是为了判断是否被引用。简单理解,如果一个对象没有被引用了,就可以回收了。

这里我们先定义一下引用(这里不包含所有“引用”的定义,比如数组会引用他的所有元素,所有对象都会引用他的Class对象等等,这里只是为了简单举例):如果一个类的对象出现在另一个类的成员里,那我们就认为后者引用了前者。比如 类 AB, 其中 B 中有一个成员变量是 A 的对象,那么就说B引用了A。如下代码:

Google Java Style 中文翻译

Google Java Style 终于在前不久发布了,抽空学习了一下,同时生成了一个java代码注释风格的中文解释,如果你有兴趣可以看看。

java source code 在此: github下载

代码中的注释基本上已经把Google Java Style中提到的一些要求写进去了,而且是出现在该出现的地方。主要是缩进的地方我跟google要求的2个字符的缩进没有保持一致, Google的这篇文档中要求的是2个字符,而我个人一直以来都是习惯使用tab来缩进,所以这个恕难从命哈。

基本上跟以前的java编码习惯差不多,就把一些Java 7之后的新的语法特性也提到了,更之前的广为流传的Sun的Java Style区别不大。如果你之前是跟Sun的保持一致的,基本上风格上不需要大改的。

其实编码风格无所谓好坏,最重要的是能让别人更容易的读懂你的代码,这并不是说每一行代码都要写注释,注释不能解决一切问题,更重要的是各种变量和名称的命名规范,这个很重要。如果代码本身就能自解释,那才是极好的!

java 线程的几种状态

java thread的运行周期中, 有几种状态, 在 java.lang.Thread.State 中有详细定义和说明:

NEW 状态是指线程刚创建, 尚未启动

RUNNABLE 状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等

BLOCKED  这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的synchronized 块的执行释放, 或者可重入的 synchronized块里别人调用wait() 方法, 也就是这里是线程在等待进入临界区

WAITING  这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束

TIMED_WAITING  这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态

TERMINATED 这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)

下面谈谈如何让线程进入以上几种状态:

1. NEW, 这个最简单了,  
 
     static void NEW() {
          Thread t = new Thread ();
         System. out.println(t.getState());
    }
 
输出NEW
 
2. RUNNABLE, 也简单, 让一个thread start, 同时代码里面不要sleep或者wait等
 
   private static void RUNNABLE() {
         Thread t = new Thread(){
             
              public void run(){
                  for(int i=0; i<Integer.MAX_VALUE; i++){
                      System. out.println(i);
                 }
             }
             
         };
         
         t.start();
    }
 
 71e94764e28c7f8bbd3ef91c1c0088b4
 
3. BLOCKED, 这个就必须至少两个线程以上, 然后互相等待synchronized 块
          
     private static void BLOCKED() {
         
          final Object lock = new Object();
         
         Runnable run = new Runnable() {
             
              @Override
              public void run() {
                  for(int i=0; i<Integer.MAX_VALUE; i++){
                      
                       synchronized (lock) {
                          System. out.println(i);
                      }
                      
                 }
             }
         };
         
         Thread t1 = new Thread(run);
         t1.setName( “t1”);
         Thread t2 = new Thread(run);
         t2.setName( “t2”);
         
         t1.start();
         t2.start();
         
    }
 
8e9ad1eadf9d38c0b6c8cb024cb36c0c
这时候, 一个在RUNNABLE, 另一个就会在BLOCKED (等待另一个线程的 System.out.println.. 这是个IO操作, 属于系统资源, 不会造成WAITING等)
 
4. WAITING, 这个需要用到生产者消费者模型, 当生产者生产过慢的时候, 消费者就会等待生产者的下一次notify
 
     private static void WAITING() {
 
          final Object lock = new Object();
         Thread t1 = new Thread(){
              @Override
              public void run() {
                 
                  int i = 0;
                 
                  while(true ){
                       synchronized (lock) {
                           try {
                               lock.wait();
                          } catch (InterruptedException e) {
                          }
                          System. out.println(i++);
                      }
                 }
             }
         };
         
         Thread t2 = new Thread(){
              @Override
              public void run() {
                 
                  while(true ){
                       synchronized (lock) {
                           for(int i = 0; i< 10000000; i++){
                              System. out.println(i);
                          }
                          lock.notifyAll();
                      }
                      
                 }
             }
         };
         
         t1.setName( “^^t1^^”);
         t2.setName( “^^t2^^”);
         
         t1.start();
         t2.start();
    }
 
 b43a3d9b67bab266ffea4537fb043bba
 
5. TIMED_WAITING, 这个仅需要在4的基础上, 在wait方法加上一个时间参数进行限制就OK了.
 
把4中的synchronized 块改成如下就可以了.
 
synchronized (lock) {
   try {
      lock.wait(60 * 1000L);
   } catch (InterruptedException e) {
   }
   System. out .println(i++);
 }
 
 88d9047d8a709c2d63c695bcf58a0297
另外看stack的输出,  他叫 TIMED_WAITING(on  object monitor) , 说明括号后面还有其他的情况, 比如sleep, 我们直接把t2的for循环改成sleep试试:
 
synchronized (lock) {
    
    try {
          sleep(30*1000L);
    } catch (InterruptedException e) {
    }
    lock.notifyAll();
}
a37ef4c72c00e793f8b6c746d74fd4d9 
 
看到了吧, t2的state是 TIMED_WAITING( sleeping),  而t1依然是on object monitor , 因为t1还是wait在等待t2 notify, 而t2是自己sleep
 
另外, join操作也是进入 on object monitor
 
6. TERMINATED, 这个状态只要线程结束了run方法, 就会进入了…
 
    private static void TERMINATED() {
         Thread t1 = new Thread();
         t1.start();
         System. out.println(t1.getState());
          try {
             Thread. sleep(1000L);
         } catch (InterruptedException e) {
         }
         System. out.println(t1.getState());
    }
输出: 
RUNNABLE
TERMINATED
 
由于线程的start方法是异步启动的, 所以在其执行后立即获取状态有可能才刚进入RUN方法且还未执行完毕
 
 
废话了这么多, 了解线程的状态究竟有什么用?
所以说这是个钓鱼贴么…
 
好吧, 一句话, 在找到系统中的潜在性能瓶颈有作用.
 
当java系统运行慢的时候, 我们想到的应该先找到性能的瓶颈, 而jstack等工具, 通过jvm当前的stack可以看到当前整个vm所有线程的状态, 当我们看到一个线程状态经常处于
WAITING 或者 BLOCKED的时候, 要小心了, 他可能在等待资源经常没有得到释放(当然, 线程池的调度用的也是各种队列各种锁, 要区分一下, 比如下图)
6db341bbd7680bbc2e6ae37a66329397
这是个经典的并发包里面的线程池, 其调度队列用的是LinkedBlockingQueue, 执行take的时候会block住, 等待下一个任务进入队列中, 然后进入执行, 这种理论上不是系统的性能瓶颈, 找瓶颈一般先找自己的代码stack,再去排查那些开源的组件/JDK的问题
 
排查问题的几个思路:
 
0. 如何跟踪一个线程?
看到上面的stack输出没有, 第一行是内容是 threadName priority tid nid desc
更过跟踪tid, nid 都可以唯一找到该线程.
 
1. 发现有线程进入BLOCK, 而且持续好久, 这说明性能瓶颈存在于synchronized块中, 因为他一直block住, 进不去, 说明另一个线程一直没有处理好, 也就这个synchronized块中处理速度比较慢, 然后再深入查看. 当然也有可能同时block的线程太多, 排队太久造成.
 
2. 发现有线程进入WAITING, 而且持续好久, 说明性能瓶颈存在于触发notify的那段逻辑. 当然还有就是同时WAITING的线程过多, 老是等不到释放.
 
3. 线程进入TIME_WAITING 状态且持续好久的, 跟2的排查方式一样.
 
 
上面的黑底白字截图都是通过jstack打印出来的, 可以直接定位到你想知道的线程的执行栈, 这对java性能瓶颈的分析是有极大作用的.
 
NOTE: 上面所有代码都是为了跟踪线程的状态而写的, 千万不要在线上应用中这么写…

struts2 获取文件上传进度方法

一、上传机制

1. struts2 的 Dispacher 类中 wrapRequest 方法, 将http请求头中带有multipart/form-data 开头的请求都会包装成为 MultiPartRequest 的实例

 Dispacher.wrapRequest

这里 MultiPartRequestWrapper 构造方法就是用MultiPartRequest 再包装一层, 同时还会调用他的parse 方法, 进行文件的上传

2. 这里getContainer().getInstance 得到的对象, 是从struts的 ObjectFactory中得到的(可以在对应的struts配置文件的<bean>标签找到), 查看struts默认配置文件 struts-default.xml 可以看到下面这一句

<bean type=“org.apache.struts2.dispatcher.multipart.MultiPartRequest” 
name=“jakarta” class=“org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest” scope=“default” optional=“true” />

这里的定义, 可以理解为:  Interface obj = cls.newInstance() 写法变成xml而已

default里面定义了两个, name 分别是 struts 和 jakarata, 作用却是完全一样的, 支持文件的分块上传, 这样大文件的上传才不会出问题.

3. 进入JakartaMultiPartRequest 类, 查看 parse方法代码

可以看出, 这里用的是 commons-fileupload 包来做文件上传的.

4. commons-fileupload 包中, ServletFileUpload 类可以使用一个 progressListener 回调对象来监听上传进度, 而这个JakartaMultiPartRequest 类中已经写死了, 没有使用任何listener:

 ServletFileUpload upload = new ServletFileUpload(fac);
 upload.setSizeMax(maxSize);
 List items = upload.parseRequest(createRequestContext(servletRequest));

这时候因为没有注入到监听器, 而且在这里会parseRequest, 也就是把文件上传过来并保存在saveDir目录中, 最后走到action,交给业务去处理。

以上是整个上传的机制,struts2通过封装commons-fileupload,直接免去了文件上传代码的编写,需要做的仅仅是在action里面保存文件以及一些后续工作而已。

但是上面的第4点中提到,因为代码写死了,没有指定监听器,也不能通过注入的形式(因为ServletFileUpload对象是临时new出来的),所以导致文件上传的过程中我们无法知道进度。

二、解决方案

5. 先理解为什么struts2为什么会使用jakarta来包装文件上传的请求,其实是在

     struts.multipart.parser 这个常量中指定的,默认情况下

   <constant name=“struts.multipart.parser” value=“jakarta”></constant>

   这里的value,就是在struts-default.xml 中定义的bean的name  (见第2节)

6. 所以解决的方法是, 我们自己来实现MultiPartRequest  这个接口, 然后自己定义一个bean, 比如叫 jakartaExt, 然后把5中的parser常量的值设置为jakartaExt, 就可以替换系统默认的包装, 用我们自己定义的了, 这个时候我们就可以给ServletFileUpload对象注入listener了.

注入listener
配置

注: 这里的uploadId仅是为了区分不同的上传线程而已(同一个用户可以开多个页面同时上传)

尝试上传文件, 可以看到日志打出来的结果:

2013-03-06 11:48:15,DEBUG,file upload, url=http://localhost/struts2/upload.htm
2013-03-06 11:48:15,DEBUG,[1362541661052] upload progress:0%
2013-03-06 11:48:28,DEBUG,[1362541661052] upload progress:10%
2013-03-06 11:48:38,DEBUG,[1362541661052] upload progress:20%
2013-03-06 11:48:48,DEBUG,[1362541661052] upload progress:30%
2013-03-06 11:48:58,DEBUG,[1362541661052] upload progress:40%
2013-03-06 11:49:08,DEBUG,[1362541661052] upload progress:50%
2013-03-06 11:49:19,DEBUG,[1362541661052] upload progress:60%
2013-03-06 11:49:30,DEBUG,[1362541661052] upload progress:70%
2013-03-06 11:49:40,DEBUG,[1362541661052] upload progress:80%
2013-03-06 11:49:52,DEBUG,[1362541661052] upload progress:90%
2013-03-06 11:50:05,DEBUG,[1362541661052] upload progress:100%
2013-03-06 11:50:05,INFO,[1362541661052] contentLength:2869568 parse use 109772ms

这样, 我们就可以通过ajax的形式, 从session中获取到对应uploadId的上传进度, 页面也就可以做出进度条了.

7. 另一种解决方案

     就是采用servlet来上传, 自己也用commons-fileupload组件来操作, 要注意的是, 把web.xml中struts2的filter-mapping 不能是 /*, 而是应该对应的 /*.action /*.htm 之类的, 否则如果拦截了所有请求, 这时候会先经过struts2的 filter而导致request被wrap掉(wrap的过程就会有parse的操作),导致servlet有响应的时候, 实际上已经上传好了…

我个人还是倾向6的做法, 因为这时候技能做到上传与业务无关, 也能监控进度, 如果在servlet中做的话, 你每上传一种文件都要一段新的处理逻辑挤进去(因为每种文件的处理逻辑不一样)

Java 慎用方法级别的synchronized关键字

为什么要这么说呢, 因为笔者被这个坑过(其实是自己坑自己)╮(╯_╰)╭

先看一段synchronized 的详解:

synchronized 是 java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

五、以上规则对其它对象锁同样适用.
简单来说, synchronized就是为当前的线程声明一个锁, 拥有这个锁的线程可以执行区块里面的指令, 其他的线程只能等待获取锁, 然后才能相同的操作.
这个很好用, 但是笔者遇到另一种比较奇葩的情况.
1. 在同一类中, 有两个方法是用了synchronized关键字声明
2. 在执行完其中一个方法的时候, 需要等待另一个方法(异步线程回调)也执行完, 所以用了一个countDownLatch来做等待
3. 代码解构如下:
synchronized void  a(){
  countDownLatch = new CountDownLatch(1);
  // do someing
  countDownLatch.await();
}

synchronized void b(){
     countDownLatch.countDown();
}
其中
a方法由主线程执行, b方法由异步线程执行后回调
执行结果是:
主线程执行 a方法后开始卡住, 不再往下做, 任你等多久都没用.
这是一个很经典的死锁问题
a等待b执行, 其实不要看b是回调的, b也在等待a执行. 为什么呢? synchronized 起了作用.
一般来说, 我们要synchronized一段代码块的时候, 我们需要使用一个共享变量来锁住, 比如:
byte[]  mutex = new byte[0];

void a1(){
     synchronized(mutex){
          //dosomething
     }
}

void b1(){

     synchronized(mutex){
          // dosomething
     }

}
如果把a方法和b方法的内容分别迁移到 a1和b1 方法的synchronized块里面, 就很好理解了.
a1执行完后会间接等待(countDownLatch)b1方法执行
然而由于 a1 中的mutex并没有释放, 就开始等待b1了, 这时候, 即使是异步的回调b1方法, 由于需要等待mutex释放锁, 所以b方法并不会执行
于是就引起了死锁
而这里的synchronized关键字放在方法前面, 起的作用就是一样的. 只是java语言帮你隐去了mutex的声明和使用而已. 同一个对象中的synchronized 方法用到的mutex是相同的, 所以即使是异步回调, 也会引起死锁, 所以要注意这个问题. 这种级别的错误是属于synchronized关键字使用不当. 不要乱用, 而且要用对.
那么这样的 隐形的mutex 对象究竟是 什么呢?
很容易想到的就是 实例本身. 因为这样就不用去定义新的对象了做锁了. 为了证明这个设想, 可以写一段程序来证明.
思路很简单, 定义一个类, 有两个方法, 一个方法声明为 synchronized, 一个在 方法体里面使用synchronized(this), 然后启动两个线程, 来分别调用这两个方法, 如果两个方法之间发生锁竞争(等待)的话, 就可以说明 方法声明的 synchronized 中的隐形的mutex其实就是 实例本身了.
public class MultiThreadSync {

    public synchronized void m1() throws InterruptedException{
         System. out.println("m1 call" );
         Thread. sleep(2000);
         System. out.println("m1 call done" );
    }

    public void m2() throws InterruptedException{
          synchronized (this ) {
             System. out.println("m2 call" );
             Thread. sleep(2000);
             System. out.println("m2 call done" );
         }
    }

    public static void main(String[] args) {
          final MultiThreadSync thisObj  = new MultiThreadSync();

         Thread t1 = new Thread(){
              @Override
              public void run() {
                  try {
                      thisObj.m1();
                 } catch (InterruptedException e) {
                      e.printStackTrace();
                 }
             }
         };

         Thread t2 = new Thread(){
              @Override
              public void run() {
                  try {
                      thisObj.m2();
                 } catch (InterruptedException e) {
                      e.printStackTrace();
                 }
             }
         };

         t1.start();
         t2.start();

    }

}
结果输出是:
m1 call
m1 call done
m2 call
m2 call done
说明方法m2的sync块等待了m1的执行. 这样就可以证实 上面的设想了.
另外需要说明的是, 当sync加在 static的方法上的时候, 由于是类级别的方法, 所以锁住的对象是当前类的class实例. 同样也可以写程序进行证明.这里略.
所以方法的synchronized 关键字, 在阅读的时候可以自动替换为synchronized(this){}就很好理解了.
                                        void method(){
void synchronized method(){                 synchronized(this){
      // biz code                               // biz code
}                             ------>>>      }
                                        }

tomcat thread dump 分析

  1. 前言

Java Thread Dump 是一个非常有用的应用诊断工具, 通过thread dump出来的信息, 可以定位到你需要了解的线程, 以及这个线程的调用栈. 如果配合linux的top命令, 可以找到你的系统中的最耗CPU的线程代码段, 这样才能有针对性地进行优化.

  1. 场景和实践

    2.1. 后台系统一直是在黑盒运行, 除了能暂停一部分任务的执行, 根本无法知道哪些任务耗CPU过多。所以一直以为是业务代码的问题, 经过各种优化(删减没必要的逻辑, 合并写操作)等等优化, 系统负载还是很高. 没什么访问量, 后台任务处理也就是每天几百万的级别, load还是达到了15以上. CPU只有4核,天天收到load告警却无从下手, 于是乎就被迫来分析一把线程.

   2.2 系统跑的是java tomcat, 要触发tomcat thread dump很简单, 先找到tomcat对应的进程id, 我们设置为PID
   【linux 命令】:  ps -ef | grep tomcat
   可以找到, 然后给这个进程发送一个QUIT的信号量, 让其触发线程的dump,  下面的操作先别急着动手, 等到看完2.3再动手不迟
    【linux 命令】: kill -3 $PID   /  kill -QUIT $PID
tomcat会把thread dump的内容输出到控制台
     【linux 命令】:cd $tomcathome/logs/
查看 catalina.out 文件, 把最后的跟thread相关的内容获取出来.
大致内容如下:
2012-04-13 16:30:41
Full thread dump OpenJDK 64-Bit Server VM (1.6.0-b09 mixed mode):
"TP-Processor12" daemon prio=10 tid=0x00000000045acc00 nid=0x7f19 in Object.wait() [0x00000000483d0000..0x00000000483d0a90]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00002aaab5bfce70> (a org.apache.tomcat.util.threads.ThreadPool$ControlRunnable)
at java.lang.Object.wait(Object.java:502)
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:662)
- locked <0x00002aaab5bfce70> (a org.apache.tomcat.util.threads.ThreadPool$ControlRunnable)
at java.lang.Thread.run(Thread.java:636)

"TP-Processor11" daemon prio=10 tid=0x00000000048e3c00 nid=0x7f18 in Object.wait() [0x00000000482cf000..0x00000000482cfd10]
java.lang.Thread.State: WAITING (on object monitor)
....
"VM Thread" prio=10 tid=0x00000000042ff400 nid=0x77de runnable"GC task thread#0 (ParallelGC)" prio=10 tid=0x000000000429c400 nid=0x77d9 runnable

"GC task thread#1 (ParallelGC)" prio=10 tid=0x000000000429d800 nid=0x77da runnable

"GC task thread#2 (ParallelGC)" prio=10 tid=0x000000000429ec00 nid=0x77db runnable

"GC task thread#3 (ParallelGC)" prio=10 tid=0x00000000042a0000 nid=0x77dc runnable

"VM Periodic Task Thread" prio=10 tid=0x0000000004348400 nid=0x77e5 waiting on condition

JNI global references: 815

Heap
PSYoungGen      total 320192K, used 178216K [0x00002aaadce00000, 0x00002aaaf1800000, 0x00002aaaf1800000)
eden space 303744K, 55% used [0x00002aaadce00000,0x00002aaae718e048,0x00002aaaef6a0000)
from space 16448K, 65% used [0x00002aaaf0690000,0x00002aaaf110c1b0,0x00002aaaf16a0000)
to   space 16320K, 0% used [0x00002aaaef6a0000,0x00002aaaef6a0000,0x00002aaaf0690000)
PSOldGen        total 460992K, used 425946K [0x00002aaab3a00000, 0x00002aaacfc30000, 0x00002aaadce00000)
object space 460992K, 92% used [0x00002aaab3a00000,0x00002aaacd9f6a30,0x00002aaacfc30000)
PSPermGen       total 56192K, used 55353K [0x00002aaaae600000, 0x00002aaab1ce0000, 0x00002aaab3a00000)
object space 56192K, 98% used [0x00002aaaae600000,0x00002aaab1c0e520,0x00002aaab1ce0000)
最后一段是系统的对内存的使用情况.
2.3. 要知道thread dump是不会告诉你每个线程的负载情况的, 需要知道每个线程的负载情况, 还得靠top命令来查看.
    【linux 命令】:top -H -p $PID
这时候, 可以看到java进程下各个线程的负载和内存等使用情况. 也不用全部搞下来, 只要top几个负载过高的记录即可(最好按下SHIFT+T 按CPU耗时总时间倒序排序,这样找到的top几个是最耗CPU时间的,而且系统启动时间应该持续15分钟以上,这样容易看出哪个线程耗时多。)
     大致内容如下:
Tasks: 118 total,   2 running, 116 sleeping,   0 stopped,   0 zombie
Cpu(s): 92.6%us,  2.3%sy,  0.0%ni,  3.8%id,  0.7%wa,  0.1%hi,  0.7%si,  0.0%st
Mem:   4054168k total,  3892212k used,   161956k free,   115816k buffers
Swap:  4192956k total,   294448k used,  3898508k free,  2156024k cachedPID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
8091 admin     16   0 1522m 814m 9660 R 22.3 20.6   4:05.61 java
8038 admin     16   0 1522m 814m 9660 R 10.3 20.6   2:46.31 java
8043 admin     15   0 1522m 814m 9660 S  3.7 20.6   1:52.04 java
8039 admin     15   0 1522m 814m 9660 S  0.7 20.6   2:10.98 java
8041 admin     15   0 1522m 814m 9660 S  0.7 20.6   1:39.66 java
8009 admin     15   0 1522m 814m 9660 S  0.3 20.6   0:27.05 java
8040 admin     15   0 1522m 814m 9660 S  0.3 20.6   0:51.46 java
7978 admin     25   0 1522m 814m 9660 S  0.0 20.6   0:00.00 java
7980 admin     19   0 1522m 814m 9660 S  0.0 20.6   0:05.05 java
7981 admin     16   0 1522m 814m 9660 S  0.0 20.6   0:06.31 java
7982 admin     15   0 1522m 814m 9660 S  0.0 20.6   0:06.50 java
7983 admin     15   0 1522m 814m 9660 S  0.0 20.6   0:06.66 java
7984 admin     15   0 1522m 814m 9660 S  0.0 20.6   0:06.87 java
7985 admin     15   0 1522m 814m 9660 S  0.0 20.6   0:33.82 java
几个字段跟top的字段意思是一致的, 就是这里的 PID是 线程在系统里面的ID, 也就是进程每创建一个线程, 不仅进程自己会分配ID, 系统也会的. 接下来的问题排查就是主要根据这个PID来走的.
看到上面的部分数据, 当前正在跑的任务中, CPU占用最高的几个线程ID
2.4. 如果不借助工具, 自己分析的话, 可以把PID字段从10进制数改为 16进制, 然后到threaddump日志中去查找一把, 找对对应的线程上下文信息, 就可以知道哪段代码耗CPU最多了.
比如 8091  的16进制是 1F9B, 查找 thread dump 日志中, nid=0x1F9B 的线程( 这里的nid意思是nativeid, 也就是上面讲的系统为线程分配的ID), 然后找到相关的代码段, 进行优化即可.
比如
"链路检测" prio=10 tid=0x00002aaafa498000 nid=0x1F9B runnable [0x0000000045fac000..0x0000000045facd10]</div>

java.lang.Thread.State: RUNNABLE
at cn.emay.sdk.communication.socket.AsynSocket$CheckConnection.run(AsynSocket.java:112)
at java.lang.Thread.run(Thread.java:636)
可以看出, 这是一个 发短信的客户端的链路检测引擎的系统负载飙升. (实际上这个线程引起的负载绝不止这么一点.)
2.5 第三方的jar包, 我感到顿时泪奔. 接下来是反编译, 看详细的代码… 果然是有一段死循环监听的… 目前是像他们要一份SDK的源代码, 或者要他们进行优化。
2.6 使用工具的话, 可以看到更多一点的信息, java的tda工具就是专门分析thread dump的.
具体功能自己去挖掘啦.

Java如何等待子线程执行结束

工作中往往会遇到异步去执行某段逻辑, 然后先处理其他事情, 处理完后再把那段逻辑的处理结果进行汇总的产景, 这时候就需要使用线程了。
一个线程启动之后, 是异步的去执行需要执行的内容的, 不会影响主线程的流程,  往往需要让主线程指定后, 等待子线程的完成. 这里有几种方式.
站在 主线程的角度, 我们可以分为主动式和被动式.
主动式指主线主动去检测某个标志位, 判断子线程是否已经完成. 被动式指主线程被动的等待子线程的结束, 很明显, 比较符合人们的胃口. 就是你事情做完了, 你告诉我, 我汇总一下, 哈哈.
那么主线程如何等待子线程工作完成呢. 很简单, Thread 类给我们提供了join 系列的方法, 这些方法的目的就是等待当前线程的die. 举个例子:

public class Threads {
    public static void main(String[] args) {
        SubThread thread = new SubThread();
        thread.start();
        //主线程处理其他工作,让子线程异步去执行.
        mainThreadOtherWork();
        System.out.println("now waiting sub thread done.");
        //主线程其他工作完毕,等待子线程的结束, 调用join系列的方法即可(可以设置超时时间)
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("now all done.");
    }

    private static void mainThreadOtherWork() {
        System.out.println("main thread work start");
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread work done.");
    }

    public static class SubThread extends Thread {
        @Override
        public void run() {
            working();
        }

        private void working() {
            System.out.println("sub thread start working.");
            busy();
            System.out.println("sub thread stop working.");
        }

        private void busy() {
            try {
                sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

本程序的数据有可能是如下:

main thread work start
sub thread start working.
main thread work done.
now waiting sub thread done.
sub thread stop working.
now all done.

忽略标号, 当然输出也有可能是1和2调换位置了. 这个我们是无法控制的. 我们看下线程的join操作, 究竟干了什么.

public final void join() throws InterruptedException {
   join(0);
}

这里是调用了

public final synchronized void join(long millis)
    throws InterruptedException

方法, 参数为0, 表示没有超时时间, 等到线程结束为止. join(millis)方法里面有这么一段代码:

        while (isAlive()) {
                wait(0);
        }

说明, 当线程处于活跃状态的时候, 会一直等待, 直到这里的isAlive方法返回false, 才会结束.isAlive方法是一个本地方法, 他的作用是判断线程是否已经执行结束. 注释是这么写的:

Tests if this thread is alive. A thread is alive if it has been started and has not yet died.

可见, join系列方法可以帮助我们等待一个子线程的结束.
那么要问, 有没有另外一种方法可以等待子线程结束? 当然有的, 我们可以使用并发包下面的Future模式.
Future是一个任务执行的结果, 他是一个将来时, 即一个任务执行, 立即异步返回一个Future对象, 等到任务结束的时候, 会把值返回给这个future对象里面. 我们可以使用ExecutorService接口来提交一个线程.

public class Threads {

    static ExecutorService executorService = Executors.newFixedThreadPool(1);

    @SuppressWarnings("rawtypes")
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        SubThread thread = new SubThread();
        // thread.start();
        Future future = executorService.submit(thread);
        mainThreadOtherWork();
        System.out.println("now waiting sub thread done.");
        future.get();
        // try {
        // thread.join();
        // } catch (InterruptedException e) {
        // e.printStackTrace();
        // }
        System.out.println("now all done.");
        executorService.shutdown();
    }

    private static void mainThreadOtherWork() {
        System.out.println("main thread work start");
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread work done.");
    }

    public static class SubThread extends Thread {
        @Override
        public void run() {
            working();
        }

        private void working() {
            System.out.println("sub thread start working.");
            busy();
            System.out.println("sub thread stop working.");
        }

        private void busy() {
            try {
                sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

这里, ThreadPoolExecutor 是实现了 ExecutorService的方法, sumbit的过程就是把一个Runnable接口对象包装成一个 Callable接口对象, 然后放到 workQueue里等待调度执行. 当然, 执行的启动也是调用了thread的start来做到的, 只不过这里被包装掉了. 另外, 这里的thread是会被重复利用的, 所以这里要退出主线程, 需要执行以下shutdown方法以示退出使用线程池. 扯远了. 
这种方法是得益于Callable接口和Future模式, 调用future接口的get方法, 会同步等待该future执行结束, 然后获取到结果. Callbale接口的接口方法是 V call(); 是可以有返回结果的, 而Runnable的 void run(), 是没有返回结果的. 所以, 这里即使被包装成Callbale接口, future.get返回的结果也是null的.如果需要得到返回结果, 建议使用Callable接口.
通过队列来控制线程的进度, 是很好的一个理念. 我们完全可以自己搞个队列, 自己控制. 这样也可以实现. 不信看代码:

public class Threads {

    // static ExecutorService executorService = Executors.newFixedThreadPool(1);
    static final BlockingQueue queue = new ArrayBlockingQueue(1);

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        SubThread thread = new SubThread(queue);
        thread.start();
        // Future future = executorService.submit(thread);
        mainThreadOtherWork();
        System.out.println("now waiting sub thread done.");
        // future.get();
        queue.take();
        // try {
        // thread.join();
        // } catch (InterruptedException e) {
        // e.printStackTrace();
        // }
        System.out.println("now all done.");
        // executorService.shutdown();
    }

    private static void mainThreadOtherWork() {
        System.out.println("main thread work start");
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread work done.");
    }

    public static class SubThread extends Thread {

        private BlockingQueue queue;

        /**
         * @param queue
         */
        public SubThread(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            try {
                working();
            } finally {
                try {
                    queue.put(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }

        private void working() {
            System.out.println("sub thread start working.");
            busy();
            System.out.println("sub thread stop working.");
        }

        private void busy() {
            try {
                sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

这里是得益于我们用了一个阻塞队列, 他的put操作和take操作都会阻塞(同步), 在满足条件的情况下.当我们调用take()方法时, 由于子线程还没结束, 队列是空的, 所以这里的take操作会阻塞, 直到子线程结束的时候, 往队列里面put了个元素, 表明自己结束了. 这时候主线程的take()就会返回他拿到的数据. 当然, 他拿到什么我们是不必去关心的.
以上几种情况都是针对子线程只有1个的时候. 当子线程有多个的时候, 情况就不妙了.
第一种方法, 你要调用很多个线程的join, 特别是当你的线程不是for循环创建的, 而是一个一个创建的时候.
第二种方法, 要调用很多的future的get方法, 同第一种方法.
第三种方法, 比较方便一些, 只需要每个线程都在queue里面 put一个元素就好了.但是, 第三种方法, 这个队列里的对象, 对我们是毫无用处, 我们为了使用队列, 而要不明不白浪费一些内存, 那有没有更好的办法呢?
有的, concurrency包里面提供了好多有用的东东, 其中, CountDownLanch就是我们要用的.
CountDownLanch 是一个倒数计数器, 给一个初始值(>=0), 然后每countDown一次就会减1, 这很符合等待多个子线程结束的场景: 一个线程结束的时候, countDown一次, 直到所有都countDown了 , 那么所有子线程就都结束了.
先看看CountDownLanch有哪些方法:

CountDownLatch

await: 会阻塞等待计数器减少到0位置. 带参数的await是多了等待时间.
countDown: 将当前的技术减1
getCount(): 返回当前的计数
显而易见, 我们只需要在子线程执行之前, 赋予初始化countDownLanch, 并赋予线程数量为初始值.
每个线程执行完毕的时候, 就countDown一下.主线程只需要调用await方法, 可以等待所有子线程执行结束, 看代码:

public class Threads {
// static ExecutorService executorService = Executors.newFixedThreadPool(1);
    static final BlockingQueue queue = new ArrayBlockingQueue(1);
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        int threads = 5;
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for(int i=0;i < threads;i++){
            SubThread thread = new SubThread(2000*(i+1), countDownLatch);
            thread.start();
        }
        mainThreadOtherWork();
        System.out.println("now waiting sub thread done.");
        countDownLatch.await();
        System.out.println("now all done.");
    }

    private static void mainThreadOtherWork() {
        System.out.println("main thread work start");
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread work done.");
    }

    public static class SubThread extends Thread{

        // private BlockingQueue queue;
        private CountDownLatch countDownLatch;
        private long work;


        public SubThread(long work, CountDownLatch countDownLatch) {
            // this.queue = queue;
            this.countDownLatch = countDownLatch;
            this.work = work;
        }

        @Override
        public void run() {
            try{
                working();
            }finally{
                countDownLatch.countDown();
            }
        }

        private void working() {
            System.out.println(getName()+" sub thread start working.");
            busy();
            System.out.println(getName()+" sub thread stop working.");
        }

        private void busy() {
            try {
                sleep(work);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

此种方法也适用于使用 ExecutorService summit 的任务的执行.
另外还有一个并发包的类CyclicBarrier, 这个是(子)线程之间的互相等待的利器. 栅栏, 就是把大家都在一个地方堵住, 就像水闸, 等大家都完成了之前的操作, 在一起继续下面的操作. 不过就不再本篇的讨论范围内了.

基于zookeeper实现的分布式锁

A distributed lock base on zookeeper.

zookeeper是hadoop下面的一个子项目, 用来协调跟hadoop相关的一些分布式的框架, 如hadoop, hive, pig等, 其实他们都是动物, 所以叫zookeeper(本人歪歪).

zookeeper其实是集群中每个节点都维护着一棵相同的树, 树的结构跟linux的目录结构的概念差不多, 以/为跟节点, 下边可以扩展任意的节点和叶子节点, 每个节点都可以写入数据. 基于zookeeper的分布式锁的实现, 其实是得益于zookeeper同步文件的强大性, 我们相信每时每刻我们访问zookeeper的树时, 相同节点返回的数据都是一致的. 这要靠zookeeper内部的一些算法来实现. 特别是leader的选举算法, 这里就不说了, 感兴趣的话可以去搜索一下看看.

我们知道了zookeeper集群的每个节点的数据都是一致的, 那么我们可以通过这些节点来作为锁的标志.

首先给锁设置一下API, 至少要包含, lock(锁住), unlock(解锁), isLocked(是否锁住)三个方法

洗牌算法的实现

所谓洗牌, 就是把牌搀和整理,以便继续玩, 要的就是把原来的牌的顺序打乱, 以便游戏的公平公正性.

传统的洗牌算法是将牌一次性洗好, 然后把洗好的牌按顺序取, 这也跟现实中的洗牌比较像.

而本人实现的一个洗牌算法, 是不打乱原来的牌的顺序的, 只是在取牌的时候, 是无序(随机)的, 这也得到的牌也是乱序的, 也能得到洗牌的效果.

例如下图所示: