产生死锁的四个必要条件

(1) 互斥条件:一个资源每次只能被一个进程(线程)使用。
(2) 请求与保持条件:一个进程(线程)因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件 : 此进程(线程)已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件 : 多个进程(线程)之间形成一种头尾相接的循环等待资源关系。

可以使用 jstack或者pstack 和 gdb 工具对死锁程序进行分析。

pstack: 功能是打印输出此进程的堆栈信息。可以输出所有线程的调用关系栈

jstack:jstack是java虚拟机自带的一种堆栈跟踪工具,所以仅适用于java程序,功能跟pstack一样,但是更强大,可以提示哪个地方可能死锁了。

pstack和jstack判断死锁,都需要多执行几次命令,观察每次的输出结果,才能推测是否死锁了。

gdb

1 运行程序,设置能影响程序运行的参数和环境 ;

2 控制程序在指定的条件下停止运行;

3 当程序停止时,可以检查程序的状态;

4 当程序 crash 时,可以检查 core 文件;

5 可以修改程序的错误,并重新运行程序;

6 可以动态监视程序中变量的值;

7 可以单步执行代码,观察程序的运行状态。

线程死锁分析:

1. 连续多次执行 $pstack <PID>   其中PID是进程号

   查看每个线程的函数调用关系的堆栈,观察每个线程当前的执行点是否在等待一个锁。

   多次执行该命令,发现某些线程的当前执行点不变,总是在等待同一个锁,就可以怀疑是否死锁了。

   如果怀疑哪些线程发生死锁了,可以采用gdb 进一步attach线程并进行分析。

2. 执行$gdb -p <PID>  或者  $gdb attach <PID>

    (gdb) info thread

    (gdb) thread <thread ID>

BTW:gdb的all-stop和non-stop模式 (GDBv7.0及之上版本支持)

默认都是all-stop,即在某线程的代码里设置了断点,触发断点时所有的线程都会中断。

non-stop是多线程调试的好帮手,某个线程里的断点触发了,其它线程还照常run,可以重现死锁的场景。

想打开non-stop,可以修改~/.gdbinit,添加如下三行:

    set target-async 1
    set pagination off
    set non-stop on

或者在进入gdb以后,依次执行以上三行,可以临时开启non-stop,退出gdb后失效。(猜是这样,需要实验验证)

另外,如果多个线程走的一个代码块,停在了同一个断点上,例如只想让线程1和线程3继续,使用:

    (gdb) thread apply 3 1 continue

BTW,判断热锁。热锁是指经常被打开关闭打开关闭的锁,由于多个线程对锁或者临界区的竞争造成的。

    * 频繁的线程的上下文切换:从操作系统对线程的调度来看,当 线程在等待资源而阻塞的时候,操作系统会将之切换出来,放到等待的队列,当线程获得资源之后,调度算法会将这个线程切换进去,放到执行队列中。
    * 大量的系统调用:因为线程的上下文切换,以及热锁的竞争,或 者临界区的频繁的进出,都可能导致大量的系统调用。
    * 大部分 CPU开销用在 “系统态 ”:线程上下文切换,和系统调用,都会导致 CPU在 “系统态 ”运行,换而言之,虽然系统很忙碌,但是 CPU用在 “用户态 ”的比例较小,应用程序得不到充分的 CPU资源。  
    * 随着 CPU数目的增多,系统的性能反而下降。因为 CPU数目多,同 时运行的线程就越多,可能就会造成更频繁的线程上下文切换和系统态的 CPU开销,从而导致更糟糕的性能。 

可以通过脚本来固定时间间隔调用pstack,和查看系统资源使用情况的命令,比如top,分别将输出打印到log文件里。

当然要每次执行命令都得打印时间戳。

分析log文件,可以大概推测出热锁所在的位置。可以通过优化热锁,来提升程序的性能,例如吞吐量、响应时间。