【Java】关于死锁的一点笔记

来源:互联网 发布:linux vim vi 区别 编辑:程序博客网 时间:2024/06/06 00:11

  同步会导致一个可能的问题:死锁。当两个线程需要独立的相同资源集时,而每个线程都锁定了这些资源的不同子集,就会发生死锁。如果两个线程都不愿放弃已有的资源,就会进入无限的停止。
  
  例如,如果线程 1 锁住了A,然后尝试对B进行加锁,同时线程 2 已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程 1 永远得不到B,线程 2 也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去,这种情况就是一个死锁。
  
  在有些情况下死锁是可以避免的,下述 3 种方法可以用于避免死锁。 
  
1、加锁顺序 

  当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个示例:

Thread 1:    lock A    lock BThread 2:    wait for A    lock C(when A locked)Thread 3:    wait fro A    wait for B    wait for C  

  如果一个线程需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。 
  
  例如,线程 2 和线程 3 只有在获取了锁A之后才能尝试获取锁C(获取锁A是获取锁C的必要条件)。因为线程 1 已经拥有了锁A,所以线程 2 和 3 需要一直等到锁A被释放,然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。
  
  按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的。
  
2、加锁时限 

  另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时间内成功获取所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其他线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(加锁超时后可以先继续运行其他事情,再回来重复之前加锁的逻辑)。
  
  这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间,需要创建一个自定义锁或使用Java5中的java.util.concurrent包下的工具。  
  
3、死锁检测 

  死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。 
  
  每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等)将其记下,除此之外,每当有线程请求锁,也需要记录在这个数据结构中。 
  
  当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁 7,但是锁 7 这个时候被线程B持有,这时线程 A 就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁 1 ,请求锁 7;线程B拥有锁 7,请求锁 1 )。 
  
  当然,死锁一般要比两个线程互相持有对方的锁这种情况复杂的多。
  
  那么当检测出死锁时,这些线程该做些什么呢?
  
  一个可行的做法是释放所有锁、回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(原因同超时类似,不能从根本上减轻竞争)。
  
  更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生时设置随机的优先级。

原创粉丝点击