多线程之八:并发调试和JDK8新特性

到了这章,多线程的介绍也到尾声了,最后介绍一下多线程在Eclipse中是怎么调试的,还有常见问题的解决方案。


多线程调试的方法

    使用Eclipse进行多线程调

        -条件断点 

      多线程之八:并发调试和JDK8新特性

        多线程之八:并发调试和JDK8新特性

        多线程之八:并发调试和JDK8新特性

    多线程之八:并发调试和JDK8新特性

    以上示例断点会中断当前线程,不影响其他线程的继续执行。这种调试方法不仅限于调试多线程,对于一般的代码调试也很常用,例如断点循环体内的某一个对象。

     多线程之八:并发调试和JDK8新特性

 

    如果将设置修改成上图,则会中断整个JVM,所有线程都会被挂起。可能会造成程序卡死,不建议使用。


Thread dump及分析

    什么是Thread dump?

           Thread Dump是非常有用的诊断Java应用问题的工具。每一个Java虚拟机都有及时生成所有线程在某一点状态的thread-dump的能力,虽然各个 Java虚拟机打印的thread dump略有不同,但是大多都提供了当前活动线程的快照,及JVM中所有Java线程的堆栈跟踪信息,堆栈信息一般包含完整的类名及所执行的方法

    Thread dump特点

            1. 能在各种操作系统下使用

            2. 能在各种Java应用服务器下使用

            3. 可以在生产环境下使用而不影响系统的性能

            4. 可以将问题直接定位到应用程序的代码行上

    Thread dump 能诊断的问题

            1. 查找内存泄露,常见的是程序里load大量的数据到缓存;

            2. 发现死锁线程;

    如何抓取Thread dump

        一般当服务器挂起,崩溃或者性能底下时,就需要抓取服务器的线程堆栈(Thread Dump)用于后续的分析. 在实际运行中,往往一次 dump的信息,还不足以确认问题。为了反映线程状态的动态变化,需要接连多次做threaddump,每次间隔10-20s,建议至少产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性。

        操作系统命令获取ThreadDump:

            Windows:

                1.转向服务器的标准输出窗口并按下Control + Break组合键之后需要将线程堆栈复制到文件中;

            UNIX/ Linux

          首先查找到服务器的进程号(process id), 然后获取线程堆栈.

                1. ps ef  | grep java

                2. kill -3 <pid>

            注意:一定要谨慎一步不慎就可能让服务器进程被杀死。kill -9 命令会杀死进程。

 

            JVM 自带的工具获取线程堆栈:

                JDK自带命令行工具获取PID,再获取ThreadDump:

                1. jps  ps ef|grepjava (获取PID)

                2. jstack [-l ]<pid> | tee -a jstack.log  (获取ThreadDump)

    Thread Dump分析

       多线程之八:并发调试和JDK8新特性

    线程名称:Timer-0
    线程类型:daemon
    优先级: 10,默认是5
       jvm线程idtid=0xac190c00jvm内部线程的唯一标识(通过java.lang.Thread.getId()获取,通常用自增方式实现。)

    对应系统线程idNativeThread ID):nid=0xaef,和top命令查看的线程pid对应,不过一个是10进制,一个是16进制。(通过命令:top -H -p pid,可以查看该进程的所有线程信息)
      线程状态in Object.wait().
    起始栈地址:[0xae77d000]

      Java thread statck trace:是上面2-7行的信息。到目前为止这是最重要的数据,Java stack trace提供了大部分信息来精确定位问题根源。

        1dump 文件里,值得关注的线程状态有:

            死锁,Deadlock(重点关注) 

            执行中,Runnable   

            等待资源,Waiting on condition(重点关注) 

            等待获取监视器,Waiting on monitor entry(重点关注)

            暂停,Suspended

            对象等待中,Object.wait() 或 TIMED_WAITING

            阻塞,Blocked(重点关注)  

            停止,Parked

        2Java thread statck trace详解:

        堆栈信息应该逆向解读:程序先执行的是第7行,然后是第6行,依次类推。

            - locked <0xb3885f60> (a java.util.ArrayList)
            - waiting on <0xb3885f60> (a java.util.ArrayList) 

        也就是说对象先上锁,锁住对象0xb3885f60,然后释放该对象锁,进入waiting状态。

    案例分析

        cpu飙高,load高,响应很慢

        方案:

            - 一个请求过程中多次dump

            - 对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了。

    查找占用cpu最多的线程信息

        方案

            - 使用命令: top -H -p pidpid为被测系统的进程号),找到导致cpu高的线程id

            - 上述Top命令找到的线程id,对应着dump thread信息中线程的nid,只不过一个是十进制,一个是十六进制。

            - thread dump中,根据top命令查找的线程id,查找对应的线程堆栈信息

 

    cpu使用率不高但是响应很慢

     方案: 

            - 进行dump,查看是否有很多thread struck在了i/o、数据库等地方,定位瓶颈原因。

       请求无法响应

        方案:

            - 多次dump,对比是否所有的runnable线程都一直在执行相同的方法,如果是的,恭喜你,锁住了!

 

        死锁:

        死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的CPU占用率为零,很快会从topprstat的输出中消失

     多线程之八:并发调试和JDK8新特性

    多线程之八:并发调试和JDK8新特性

        (图1)中有一个“Waiting for monitor entry”,可以看出,两个线程各持有一个锁,又在等待另一个锁,很明显这两个线程互相持有对方正在等待的锁。所以造成了死锁现象;

        (图2)中对死锁的现象做了说明,可以看到,是“DeadLockTest.java”的39行造成的死锁现象。这样就能到相应的代码下去查看,定位问题

 

JDK8对并发的新支持

    – LongAdder

        – AtomicInteger类似的使用方式

        – AtomicInteger上进行了热点分离

        – public void add(long x)

        – public void increment()

        – public void decrement()

        – public long sum()

        – public long longValue()

        – public int intValue()

    – CompletableFuture

        – 实现CompletionStage接口(40余个方法)

        – Java 8中对Future的增强版

        – 支持流式调用

    – StampedLock

        – 读写锁的改进

        – 读不阻塞

        – StampedLock的实现思

            – CLH自旋锁

            – 锁维护一个等待线程队列,所有申请锁,但是没有成功的线程都记录在这个队列中。每一个节点(一个 节点代表一个线程),保存一个标记位(locked),用于判断当前线程是否已经释放锁。

             – 当一个线程试图获得锁时,取得当前等待队列的尾部节点作为其前序节点。并使用类似如下代码判断前 序节点是否已经成功释放锁:

            while (pred.locked) { }

            – 不会进行无休止的自旋,会在在若干次自旋后挂起线