Basic Of Concurrency(十: 死锁和预防)

线程死锁

死锁是指一到多个线程阻塞等待的锁被其余线程持有且不释放.当多个线程在同一时间按不一样的顺序来获取相同的锁的状况下,会发生死锁.html

例如,线程1持有锁A且尝试去获取锁B,而线程2持有锁B且尝试去获取锁A,那么死锁将会发生.线程1永远获取不到锁B, 而线程2则永远获取不到锁A.且它们都不知道死锁已经发生.它们将永远各自阻塞且持有锁A和B.这种状况咱们称之为死锁.java

以下描述死锁:sql

线程 1 持有锁A, 尝试获取锁B.
线程 2 持有锁B, 尝试获取锁A.
复制代码

如下是一个死锁的示例,在TreeNodes中调用不一样实例的同步方法:数据库

public class TreeNode {
  TreeNode parent   = null;  
  List     children = new ArrayList();
  public synchronized void addChild(TreeNode child){
    if(!this.children.contains(child)) {
      this.children.add(child);
      child.setParentOnly(this);
    }
  }
  
  public synchronized void addChildOnly(TreeNode child){
    if(!this.children.contains(child){
      this.children.add(child);
    }
  }
  
  public synchronized void setParent(TreeNode parent){
    this.parent = parent;
    parent.addChildOnly(this);
  }

  public synchronized void setParentOnly(TreeNode parent){
    this.parent = parent;
  }
}
复制代码

当线程1调用addChild方式时,线程2在同一时间调用setParent方法,且使用同一个parent和child实例,此时死锁发生了.数据结构

以下描述:并发

// Thread 1:
	// 此时持有parent对象锁
	parent.addChild(child);
	// 尝试去得到child对象锁
	child.setParentOnly(parent);
// Thread 2:
	// 此时持有child对象锁
	child.setParent(parent);
	// 尝试去得到parent锁
	parent.addChildOnly(child);
复制代码

当线程1成功调用parent.addChild(child)时,线程1已经进入同步代码块,并成功获取parent对象锁.其余想要获取parent对象锁的线程只能等待线程1释放.post

一样的,当线程2成功调用child.setParent(parent)时,线程2已经进入同步代码块,并成功获取child对象锁,其余想要获取child对象锁的线程只能等待线程2释放.this

如今child和parent对象锁同时被不一样的两个线程持有.当线程1尝试去调用child.setParentOnly(parent)时,此时child对象锁被线程2持有且未释放,线程1只能阻塞等待child对象锁.一样的,线程2尝试去调用parent.addChildOnly(child)时,此时parent对象锁被线程1持有且未释放,线程2只能阻塞等待parent对象锁.如今两个线程都在阻塞等待对方所持有的对象锁释放.spa

注意:以上死锁发生须要两个线程同时调用parent.addChild(child)和child.setParent(parent)且须要使用相同的parent和child对象实例.上文中说起的代码可能须要执行不少次才能让死锁发生.线程

两个线程须要在相同的时间点持有对方所须要的锁.但凡其中一个线程领先另外一个线程一点,就能成功得到parent和child对象锁,这样另外一个线程一开始就只能阻塞等待对方释放锁.这样死锁就不会发生了.因为线程的执行时机不可预测,所以咱们没法按照预期来重现死锁,只能说可能会发生死锁.

更加完整的死锁

死锁能够在多于两个线程的状况下产生.这可能比较难观察.以下示例四个线程的死锁状况:

线程 1 持有锁A, 等待锁B
线程 2 持有锁B, 等待锁C
线程 3 持有锁C, 等待锁D
线程 4 持有锁D, 等待锁A
复制代码

线程1等待线程2释放锁,线程2等待线程3释放锁,线程3等待线程4释放锁,线程4等待线程1释放锁.

数据库死锁

数据库事务是一个更加完整的死锁状况.一个事务中可能会有多个sql更新请求.当一条记录被一个事务更新时会被当前事务锁住,其余想更新同一条记录的事务只能等待持有当前行级锁的事务释放.同一个事务中的多个更新请求可能会锁住数据库中的多条记录.

若是多个事务在同一时间进行且须要更新相同的记录.那么将会有发生死锁的风险.

以下所示:

事务1, 更新请求1, 锁住记录1且进行更新
事务2, 更新请求1, 锁住记录2且进行更新 
事务1, 更新请求2, 尝试获取记录2的行级锁更新记录
事务2, 更新请求2, 尝试获取记录1的行级锁更新记录
复制代码

当记录被不一样的更新请求持有,且当前事务不能提早知道执行完当前事务所须要的所有行级锁.那么将很难在数据库事务中检查和预防死锁.

死锁预防

在如下三种措施可以用来预防死锁.

  1. 按顺序获取锁
  2. 持有锁超时
  3. 死锁检测

顺序获取锁

咱们知道死锁会发生在多个线程以不一样顺序获取相同锁的状况下.

若是咱们能确保全部线程都能以相同的顺序来获取锁,那么死锁将不会发生.

以下所示:

线程1:
	持有锁A
	持有锁B
线程2:
	等待获取锁A
	当成功获取锁A时,获取锁C
线程3:
	等待获取锁A
	等待获取锁B
	等待获取锁C
复制代码

若是一个像线程3的线程须要获取多个锁,那么线程须要按照给定的顺序来获取它们.当须要获取序列中的后一个锁以前只能先持有前一个锁.

不管是线程2仍是3都须要先获取到锁A才能继续获取锁C.当线程1已经持有锁A时,线程2和3只能等待线程1释放锁A.因此他们只有在成功获取到锁A的状况下,才能继续获取锁B和锁C.

顺序获取锁是一个比较简单且高效的预防死锁的措施.然而,这种方式只能在你知道全部用到的锁的前提下才有用,但实际状况下并不老是这样.

持有锁超时

另外一个预防死锁的方式是在线程等待获取锁的过程当中加入超时机制.即让线程等待获取锁一段时间后超时.若是线程不能成功获取它执行过程当中所须要的所有锁则超时回滚,释放全部它所持有的锁,在一个给定的随机时间后从新尝试执行.给定的随机时间让其余线程有机会去获取它们所等待获取的锁,以便让应用可以退出等待状态继续运行下去.

这里是一个让两个线程尝试以不一样的顺序获取相同的锁,且加入超时机制,让线程可以回滚和从新尝试执行的实例.

线程 1 持有锁A
线程 2 持有锁B

线程 1 尝试获取锁B进入等待状态
线程 2 尝试获取锁A进入等待状态

线程 1 等待获取锁B超时
线程 1 回滚和释放锁A
线程 1 等待随机时间(300毫秒)后从新执行

线程 2 等待获取锁A超时
线程 2 回滚和释放锁B
线程 2 等待随机时间(40毫秒)后从新执行
复制代码

以上实例中,线程2领先线程260毫秒去尝试从新执行,看起来是能够成功获取到全部锁来知足执行的.线程A将会从新等待获取锁A.当线程2执行完毕后,线程1也可以获取全部须要的锁来完成执行.(除非线程2或其余线程在线程1执行过程当中持有线程1所须要的锁,则执行失败).

有一点须要记住的是,线程获取锁超时并不意味着死锁的发生.可能只是线程持有锁后执行任务的时长过长已经超过了其余线程等待获取锁的超时时长.

此外,若是有足够的线程去竞争相同的资源,即便它们会超时和回滚,仍然有重复一次次持有其余线程所须要锁的风险.也许当只有两个线程互相等待0~500毫秒并进行重试的状况下这种风险不会放生,但若是是10到20个线程的状况下就不必定了.在线程足够多的状况下,两个线程等待重试的时间相同或是接近的概率就很高了.

更大的问题在于持有锁超时的方式在Java同步代码块中不可能实现.咱们不能为Javasynchronized添加超时机制.你可能须要自定义锁或是使用java5java.util.concurrency包中的并发数据结构来实现.

死锁检测

死锁检测是一个在顺序获取锁和持有锁超时两个措施都没法使用的状况下才使用的重量级措施.

每一次线程请求获取和成功持有锁的过程都会被记录在能够存储线程和锁关系的数据结构中(如Map).

当一个线程请求获取锁被拒绝后,线程能够遍历数据结构来检查是否发生了死锁.举个例子,线程A请求获取锁7,而锁7被线程B持有,线程A可以进行检查线程B是否须要获取线程A所持有的锁,若是须要则死锁发生(线程A持有锁1, 尝试获取锁7.线程B持有锁7尝试获取锁1).

固然死锁的发生可以在超过两个线程的状况下.线程A等待线程B,线程B等待线程C,线程C等待线程D.为了让线程A对死锁进行检测,咱们须要知道线程B所请求的全部锁;由于线程B请求的锁被线程C持有,线程C请求的锁被线程D持有,因此咱们须要知道连同线程C和D在内所请求的锁.直到线程A发现它持有线程B所须要的全部锁中的一个或多个为止,则认定死锁发生.

下图展现了4个线程和它们所请求和持有锁之间的关系.这样一个数据结构可以用来检测死锁的发生.

当检测到死锁发生时,线程须要作什么?

可让线程回滚且释放全部已持有的锁,在等待一段随机时长后从新尝试运行.这种方式相似于持有锁超时措施,区别在于仅在检测到死锁真实发生时,线程才会触发回滚.然而在竞争相同锁的线程过多的状况下,仍然会在屡次回滚和等待后从新进入死锁状态.

一个更加恰当的方式是为线程分配优先级,只让一个或少数几个线程进行回滚.在这回滚的片刻时间里,死锁得以解决,让其余线程能够获取它们所须要的锁继续执行.若是给线程分配的优秀级是固定的,一样的线程可能会一直占据高优先级.固然咱们能够在检测到死锁时随机分配优先级来解决这个问题.

该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: 线程通信
下一篇: 饥饿与公平

相关文章
相关标签/搜索