线程死锁——死锁产生的条件

x33g5p2x  于2022-07-10 转载在 其他  
字(1.7k)|赞(0)|评价(0)|浏览(471)

什么是线程死锁

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于相互等待状态,若无外力作用,它们将无法继续执行下去。

造成死锁的原因可以概括成三句话:

  • 当前线程拥有其他线程需要的资源
  • 当前线程等待其他线程已拥有的资源
  • 不放弃自己拥有的资源

线程死锁产生的四个必要条件

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用;
  2. 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  3. 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  4. 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

举个必然产生死锁的例子

public static void main(String[] args) {
	   Object a = new Object();
	   Object b = new Object();
	
	   // 线程1
	   new Thread(() -> {
	      synchronized (a) {
	          System.out.println("获得了A锁");
	          try {
	              Thread.sleep(1000);
	          } catch (InterruptedException e) {
	              e.printStackTrace();
	          }
	          synchronized (b) {
	
	          }
	      }
	   }).start();
	   
	   // 线程2
	   new Thread(() -> {
	       synchronized (b) {
	           System.out.println("获得了B锁");
	           try {
	               Thread.sleep(1000);
	           } catch (InterruptedException e) {
	               e.printStackTrace();
	           }
	           synchronized (a) {
	
	           }
	       }
	   }).start();
}

上面的程序就是一个典型死锁的例子,为了保证死锁发生的几率,我这里在获得锁之后睡眠了1s。

线程1在获得A对象锁之后等了1s去尝试获取B对象锁,这时线程1是持有A对象锁的;线程2在获得B对象锁之后等待1s去尝试获得A对象锁,这时线程2是持有B对象锁的;就在它们彼此想获得对方的锁的时候,死锁发生了,并且一直持续下去。

如何避免死锁

上面提到只有这四个条件都发生时才会出现死锁,那么意思就是说,只要我们破坏其中一个,就可以成功预防死锁的发生。

  • 破坏互斥:只有一把锁,这是形成死锁的最关键的原因。显然,如果我们能在两个线程跑之前,能给每个线程单独拷贝一份钥匙的副本,就能有效的避免死锁了。
  • 占用且等待一次性申请所有的资源,这样就不存在等待了。
    例如线程1一次性拿到A和B两个锁,线程2在获取锁的时候需要等待线程1释放锁,这样就避免了多线程互相占用等待的情况。
  • 不可抢占:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
    在上面的死锁代码中,我们使用了synchronized关键字,它是不能主动释放资源的,会造成线程一直阻塞,JUC提供了Lock解决这个问题。
    显式使用Lock类中的定时tryLock功能来代替内置锁机制,可以检测死锁和从死锁中恢复过来。显式锁可以指定一个超时时限(Timeout),在等待超过该时间后tryLock就会返回一个失败信息,释放其拥有的资源,其他线程可以获取此资源避免死锁。
  • 循环等待:如果一个线程需要一些锁,那么它必须按照确定的顺序获取锁。只有先获得了从顺序上排在前面的锁之后,才能获取后面的锁。
    破坏循环条件很简单,只要线程之间不要出现交叉占用的情况即可,也就是说在在代码中尽量避免线程1保持A请求B,线程2保持B请求A,尽可能使他们请求的顺序一致,比如线程1请求的顺序是A、B,线程2请求的顺序也是A、B,这样自然就避免了循环等待的情况发生。

总结

死锁是一个比较头疼的问题,但是只要我们的代码规范,可以避免大多数情况下的死锁。还有避免死锁的经典算法是银行家算法,这里就不扩开介绍了。

在很多情况下,尤其是多线程编程中,我们要注意线程之间的资源是否存在互相竞争的情况,如果有,要及时规避死锁的风险。

死锁很多时候会发生在数据库操作中,例如长事务、并发条件下的共享锁升级等都会造成数据库死锁,后面有时间会专门针对数据库死锁讲一讲。

相关文章