死锁问题
所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
特殊的概念
饥饿指的线程无法访问到它需要的资源而不能继续执行时,引发饥饿最常见资源就是CPU时钟周期。
- 在程序中使用的Thread.yield或者Thread.sleep(0)表明该程序试图克服优先级调整问题,让优先级更低的线程拥有被CPU调度的机会。
- Thread.sleep(0)不等于不写
活锁指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。
活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务将回滚整个操作。解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得程序可以继续执行其他的任务。
Java中活锁和死锁有什么区别?
活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。
怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。
死锁产生的原因
- 系统资源的竞争。通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。
- 不剥夺性条件。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
- 进程推进顺序非法。进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
- 1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
- 3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, Pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。
如何避免死锁
加锁顺序(线程按照一定的顺序加锁,避免嵌套封锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
发现死锁
死循环、死锁、阻塞、页面打开慢等问题,打印线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
- 获取到线程的进程编号pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
- 打印进程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
死锁检测
Jconsole是JDK自带的图形化界面工具,使用JDK给我们的的工具JConsole,可以通过打开cmd然后输入jconsole打开,检测出该进程中造成死锁的线程
Jstack是JDK自带的命令行工具,主要用于线程Dump分析。
1、先用Jps来查看java进程id,例如6666
2、jstack输出线程dump信息到文件 jstack -l 6666 > aa.log
3、查看dump文件,然后进行分析。可以看到提示【Found 1 deadlock】,可以看到报错行数位置
总结
1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。
2、设计时考虑清楚锁的顺序,尽量减少嵌入加锁交互数量。
3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。