忽然发现我的图解零碎缺了「死锁」的内容,这就来补下。

在面试过程中,死锁也是高频的考点,因为如果线上环境真多产生了死锁,那真的出小事了。

这次,咱们就来系统地聊聊死锁的问题。

  • 死锁的概念;
  • 模仿死锁问题的产生;
  • 利用工具排查死锁问题;
  • 防止死锁问题的产生;

死锁的概念

在多线程编程中,咱们为了避免多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有胜利取得到锁的线程,能力操作共享资源,获取不到锁的线程就只能期待,直到锁被开释。

那么,当两个线程为了爱护两个不同的共享资源而应用了两个互斥锁,那么这两个互斥锁利用不当的时候,可能会造成两个线程都在期待对方开释锁,在没有外力的作用下,这些线程会始终互相期待,就没方法持续运行,这种状况就是产生了死锁

举个例子,小林拿了小美房间的钥匙,而小林在本人的房间里,小美拿了小林房间的钥匙,而小美也在本人的房间里。如果小林要从本人的房间里进来,必须拿到小美手中的钥匙,然而小美要进来,又必须拿到小林手中的钥匙,这就造成了死锁。

死锁只有同时满足以下四个条件才会产生:

  • 互斥条件;
  • 持有并期待条件;
  • 不可剥夺条件;
  • 环路期待条件;
互斥条件

互斥条件是指多个线程不能同时应用同一个资源

比方下图,如果线程 A 曾经持有的资源,不能再同时被线程 B 持有,如果线程 B 申请获取线程 A 曾经占用的资源,那线程 B 只能期待,直到线程 A 开释了资源。

持有并期待条件

持有并期待条件是指,当线程 A 曾经持有了资源 1,又想申请资源 2,而资源 2 曾经被线程 C 持有了,所以线程 A 就会处于期待状态,然而线程 A 在期待资源 2 的同时并不会开释本人曾经持有的资源 1

不可剥夺条件

不可剥夺条件是指,当线程曾经持有了资源 ,在本人应用完之前不能被其余线程获取,线程 B 如果也想应用此资源,则只能在线程 A 应用完并开释后能力获取。

环路期待条件

环路期待条件指都是,在死锁产生的时候,两个线程获取资源的程序形成了环形链

比方,线程 A 曾经持有资源 2,而想申请资源 1, 线程 B 曾经获取了资源 1,而想申请资源 2,这就造成资源申请期待的环形图。


模仿死锁问题的产生

Talk is cheap. Show me the code.

上面,咱们用代码来模仿死锁问题的产生。

首先,咱们先创立 2 个线程,别离为线程 A 和 线程 B,而后有两个互斥锁,别离是 mutex_A 和 mutex_B,代码如下:

pthread_mutex_t mutex_A = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t mutex_B = PTHREAD_MUTEX_INITIALIZER;int main(){    pthread_t tidA, tidB;        //创立两个线程    pthread_create(&tidA, NULL, threadA_proc, NULL);    pthread_create(&tidB, NULL, threadB_proc, NULL);        pthread_join(tidA, NULL);    pthread_join(tidB, NULL);        printf("exit\n");        return 0;}

接下来,咱们看下线程 A 函数做了什么。

//线程函数 Avoid *threadA_proc(void *data){    printf("thread A waiting get ResourceA \n");    pthread_mutex_lock(&mutex_A);    printf("thread A got ResourceA \n");        sleep(1);        printf("thread A waiting get ResourceB \n");    pthread_mutex_lock(&mutex_B);    printf("thread A got ResourceB \n");    pthread_mutex_unlock(&mutex_B);    pthread_mutex_unlock(&mutex_A);    return (void *)0;}

能够看到,线程 A 函数的过程:

  • 先获取互斥锁 A,而后睡眠 1 秒;
  • 再获取互斥锁 B,而后开释互斥锁 B;
  • 最初开释互斥锁 A;
//线程函数 Bvoid *threadB_proc(void *data){    printf("thread B waiting get ResourceB \n");    pthread_mutex_lock(&mutex_B);    printf("thread B got ResourceB \n");        sleep(1);        printf("thread B waiting  get ResourceA \n");    pthread_mutex_lock(&mutex_A);    printf("thread B got ResourceA \n");        pthread_mutex_unlock(&mutex_A);    pthread_mutex_unlock(&mutex_B);    return (void *)0;}

能够看到,线程 B 函数的过程:

  • 先获取互斥锁 B,而后睡眠 1 秒;
  • 再获取互斥锁 A,而后开释互斥锁 A;
  • 最初开释互斥锁 B;

而后,咱们运行这个程序,运行后果如下:

thread B waiting get ResourceB thread B got ResourceB thread A waiting get ResourceA thread A got ResourceA thread B waiting get ResourceA thread A waiting get ResourceB // 阻塞中。。。

能够看到线程 B 在期待互斥锁 A 的开释,线程 A 在期待互斥锁 B 的开释,单方都在期待对方资源的开释,很显著,产生了死锁问题。


利用工具排查死锁问题

如果你想排查你的 Java 程序是否死锁,则能够应用 jstack 工具,它是 jdk 自带的线程堆栈剖析工具。

因为小林的死锁代码例子是 C 写的,在 Linux 下,咱们能够应用 pstack + gdb 工具来定位死锁问题。

pstack 命令能够显示每个线程的栈跟踪信息(函数调用过程),它的应用形式也很简略,只须要 pstack <pid> 就能够了。

那么,在定位死锁问题时,咱们能够屡次执行 pstack 命令查看线程的函数调用过程,屡次比照后果,确认哪几个线程始终没有变动,且是因为在期待锁,那么大概率是因为死锁问题导致的。

我用 pstack 输入了我后面模仿死锁问题的过程的所有线程的状况,我屡次执行命令后,其后果都一样,如下:

$ pstack 87746Thread 3 (Thread 0x7f60a610a700 (LWP 87747)):#0  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0#1  0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0#2  0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0#3  0x0000000000400725 in threadA_proc ()#4  0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0#5  0x00000037206f4bfd in clone () from /lib64/libc.so.6Thread 2 (Thread 0x7f60a5709700 (LWP 87748)):#0  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0#1  0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0#2  0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0#3  0x0000000000400792 in threadB_proc ()#4  0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0#5  0x00000037206f4bfd in clone () from /lib64/libc.so.6Thread 1 (Thread 0x7f60a610c700 (LWP 87746)):#0  0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0#1  0x0000000000400806 in main ()....$ pstack 87746Thread 3 (Thread 0x7f60a610a700 (LWP 87747)):#0  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0#1  0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0#2  0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0#3  0x0000000000400725 in threadA_proc ()#4  0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0#5  0x00000037206f4bfd in clone () from /lib64/libc.so.6Thread 2 (Thread 0x7f60a5709700 (LWP 87748)):#0  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0#1  0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0#2  0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0#3  0x0000000000400792 in threadB_proc ()#4  0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0#5  0x00000037206f4bfd in clone () from /lib64/libc.so.6Thread 1 (Thread 0x7f60a610c700 (LWP 87746)):#0  0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0#1  0x0000000000400806 in main ()

能够看到,Thread 2 和 Thread 3 始终阻塞获取锁(pthread_mutex_lock)的过程,而且 pstack 屡次输入信息都没有变动,那么可能大概率产生了死锁。

然而,还不可能确认这两个线程是在相互期待对方的锁的开释,因为咱们看不到它们是等在哪个锁对象,于是咱们能够应用 gdb 工具进一步确认。

整个 gdb 调试过程,如下:

// gdb 命令$ gdb -p 87746// 打印所有的线程信息(gdb) info thread  3 Thread 0x7f60a610a700 (LWP 87747)  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0  2 Thread 0x7f60a5709700 (LWP 87748)  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0* 1 Thread 0x7f60a610c700 (LWP 87746)  0x0000003720e080e5 in pthread_join () from /lib64/libpthread.so.0//最右边的 * 示意 gdb 锁定的线程,切换到第二个线程去查看// 切换到第2个线程(gdb) thread 2[Switching to thread 2 (Thread 0x7f60a5709700 (LWP 87748))]#0  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0 // bt 能够打印函数堆栈,却无奈看到函数参数,跟 pstack 命令一样 (gdb) bt#0  0x0000003720e0da1d in __lll_lock_wait () from /lib64/libpthread.so.0#1  0x0000003720e093ca in _L_lock_829 () from /lib64/libpthread.so.0#2  0x0000003720e09298 in pthread_mutex_lock () from /lib64/libpthread.so.0#3  0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:25#4  0x0000003720e07893 in start_thread () from /lib64/libpthread.so.0#5  0x00000037206f4bfd in clone () from /lib64/libc.so.6// 打印第三帧信息,每次函数调用都会有压栈的过程,而 frame 则记录栈中的帧信息(gdb) frame 3#3  0x0000000000400792 in threadB_proc (data=0x0) at dead_lock.c:2527    printf("thread B waiting get ResourceA \n");28    pthread_mutex_lock(&mutex_A);// 打印mutex_A的值 ,  __owner示意gdb中标示线程的值,即LWP(gdb) p mutex_A$1 = {__data = {__lock = 2, __count = 0, __owner = 87747, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},   __size = "\002\000\000\000\000\000\000\000\303V\001\000\001", '\000' <repeats 26 times>, __align = 2}// 打印mutex_B的值 ,  __owner示意gdb中标示线程的值,即LWP(gdb) p mutex_B$2 = {__data = {__lock = 2, __count = 0, __owner = 87748, __nusers = 1, __kind = 0, __spins = 0, __list = {__prev = 0x0, __next = 0x0}},   __size = "\002\000\000\000\000\000\000\000\304V\001\000\001", '\000' <repeats 26 times>, __align = 2}  

我来解释下,下面的调试过程:

  1. 通过 info thread 打印了所有的线程信息,能够看到有 3 个线程,一个是主线程(LWP 87746),另外两个都是咱们本人创立的线程(LWP 87747 和 87748);
  2. 通过 thread 2,将切换到第 2 个线程(LWP 87748);
  3. 通过 bt,打印线程的调用栈信息,能够看到有 threadB_proc 函数,阐明这个是线程 B 函数,也就说 LWP 87748 是线程 B;
  4. 通过 frame 3,打印调用栈中的第三个帧的信息,能够看到线程 B 函数,在获取互斥锁 A 的时候阻塞了;
  5. 通过 p mutex_A,打印互斥锁 A 对象信息,能够看到它被 LWP 为 87747(线程 A) 的线程持有着;
  6. 通过 p mutex_B,打印互斥锁 A 对象信息,能够看到他被 LWP 为 87748 (线程 B) 的线程持有着;

因为线程 B 在期待线程 A 所持有的 mutex_A, 而同时线程 A 又在期待线程 B 所领有的mutex_B, 所以能够判定该程序产生了死锁。


防止死锁问题的产生

后面咱们提到,产生死锁的四个必要条件是:互斥条件、持有并期待条件、不可剥夺条件、环路期待条件。

那么防止死锁问题就只须要破环其中一个条件就能够,最常见的并且可行的就是应用资源有序调配法,来破环环路期待条件

那什么是资源有序调配法呢?

线程 A 和 线程 B 获取资源的程序要一样,当线程 A 是先尝试获取资源 A,而后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,而后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以雷同的程序申请本人想要的资源。

咱们应用资源有序调配法的形式来批改后面产生死锁的代码,咱们能够不改变线程 A 的代码。

咱们先要分明线程 A 获取资源的程序,它是先获取互斥锁 A,而后获取互斥锁 B。

所以咱们只需将线程 B 改成以雷同程序的获取资源,就能够突破死锁了。

线程 B 函数改良后的代码如下:

//线程 B 函数,同线程 A 一样,先获取互斥锁 A,而后获取互斥锁 Bvoid *threadB_proc(void *data){    printf("thread B waiting get ResourceA \n");    pthread_mutex_lock(&mutex_A);    printf("thread B got ResourceA \n");        sleep(1);        printf("thread B waiting  get ResourceB \n");    pthread_mutex_lock(&mutex_B);    printf("thread B got ResourceB \n");        pthread_mutex_unlock(&mutex_B);    pthread_mutex_unlock(&mutex_A);    return (void *)0;}

执行后果如下,能够看,没有产生死锁。

thread B waiting get ResourceA thread B got ResourceA thread A waiting get ResourceA thread B waiting  get ResourceB thread B got ResourceB thread A got ResourceA thread A waiting get ResourceB thread A got ResourceBexit

总结

简略来说,死锁问题的产生是由两个或者以上线程并行执行的时候,抢夺资源而相互期待造成的。

死锁只有同时满足互斥、持有并期待、不可剥夺、环路期待这四个条件的时候才会产生。

所以要防止死锁问题,就是要毁坏其中一个条件即可,最罕用的办法就是应用资源有序调配法来毁坏环路期待条件。