在安全性与活跃性之间一般存在着某些制衡。java
锁顺序死锁(Lock-Ordering Deadlock)
。资源死锁(Resource Deadlock)
。在数据库系统的设计中考虑了检测死锁以及从死锁中恢复。当数据库系统检测到一组事务发生了死锁(经过在表示等待关系的有向图中搜索循环),将选择一个牺牲者,释放其所有资源,放弃这个事务。
JVM解决死锁的问题远没有数据库服务那么强大,当一组java线程发生死锁时,这些线程将永远不能再使用了。根据线程完成工做的不一样,可能形成应用程序彻底中止,或者某个特意子系统中止,或是性能下降。恢复应用程序的惟一方式就是停止并重启它。
与其余的并发危险同样,一个类有可能发生死锁,并非每次都会发生死锁。在死锁发生的时候,每每是最糟糕的时候--高负载状况下。数据库
这个是最多见的死锁发生方式:线程1和线程2,线程1调用A(),得到了锁A,想要继续调用B();线程2调用了B(),得到了锁B,想要继续调用A()。两个线程都在等待对方释放资源。
这里死锁的缘由,是由于两个线程试图以不一样的顺序得到相同的锁。若是按照相同的顺序来请求锁,那么就不会出现循环的加锁依赖性。也就不会发生死锁了。安全
若是全部线程以固定的顺序来得到锁,那么程序中就不会出现锁顺序死锁的问题。
有时候,并不能清楚地知道是否在锁顺序上有足够的控制权来避免死锁的发生。并发
/** * 容易发生死锁 */ public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { synchronized(fromAccount){ synchronized(toAccount){ if(fromAccount.getBalance.compareTo(account) < 0){ throw new InsufficientFundsException(); } else { fromAccount.debit(amount); toAccount.credit(amount); } } } }
全部的线程看起来都是按照相同的顺序来获取锁,事实上锁的顺序取决于传递给transferMoney的顺序。若是一个线程从X向Y转帐,另外一个线程从Y向X转帐,那么就有可能发送锁顺序死锁。ide
private static final Object tieLock = new Object(); public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException { class Helper{ public void transfer() throws throws InsufficientFundsException { if(fromAccount.getBalance.compareTo(account) < 0){ throw new InsufficientFundsException(); } else { fromAccount.debit(amount); toAccount.credit(amount); } } } int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); if(fromHash < toHash){ synchronized(fromAccount){ synchronized(toAccount){ new Helper().transfer(); } } } else if(fromHash > toHash){ synchronized(toAccount){ synchronized(fromAcct){ new Helper().transfer(); } } }else{ synchronized(tieLock){ synchronized(toAccount){ synchronized(fromAcct){ new Helper().transfer(); } } } } }
在极少数状况下,两个对象可能拥有相同的散列值,此时必须经过某种任务的方法来决定锁的顺序,不然可能又会陷入死锁。为了不这种状况,引入了“加时赛(Tie-Breaking)”锁。在获取两个锁以前,首先得到这个“加时赛”锁,从而保证只有一个线程以未知的顺序得到这两个锁,从而消除了死锁发生的可能性。
若是Account中包含一个惟一的,不可变的且具有可比性的键值,好比id,帐号,那么只须要根据键值对对象进行排序,不须要使用“加时赛”锁。性能
若是在持有锁时调用某个外部方法,那么将出现活跃性问题。在外部方法中可能得到其余锁(这可能会产生死锁),或阻塞时间过长,致使其余线程没法即便得到当前被持有的锁。
方法调用至关于一种抽象屏障,由于你无需了解被调用方法中所执行的操做。但也正是因为不知道在被调用方法中执行的操做,所以在持有锁的时候对调用某个外部方法难以进行分析,从而可能出现死锁。
若是在调用某个方法时不须要持有锁,那么这种调用被称为开放调用(Open Call)
。依赖于开放调用的类一般能表现出更好的行为,而且与那些在调用方法时须要持有锁的类相比,也更容易编写。经过尽量的使用开放调用,将更容易找出那些须要获取多个锁的代码路径,所以也更容易确保采用一致的顺序来获取锁。操作系统
在程序中应尽可能使用开放调用。与那些在持有锁时调用外部方法的程序相比,更容易对依赖于开放调用的程序进行死锁分析。
正如当多个线程相互持有彼此正在等待的锁而又不释放本身已持有的锁时会发生死锁,当它们在相同的资源集合上等待时,也会发生死锁。
假设有两个资源池,例如两个不一样数据库的链接池。资源池一般采用信号量来实现当资源池为空时的阻塞行为。若是一个任务须要链接两个数据库,而且在请求这两个资源时不会始终遵照相同的顺序,那么也可能出现死锁(资源池越大,死锁几率越小)。
另外一种基于资源的死锁形式就是线程饥饿死锁
:一个任务提交另外一个任务,并等待被提交任务在单线程的Executor中执行完成。这种状况下,第一个任务将永远的等待下去,并使得另外一个任务以及在这个Executor中执行的全部其余任务都中止执行。若是某些任务须要等待其余任务的结果,那么这些任务每每是产生线程饥饿死锁的主要来源。有界线程池/资源池与相互依赖的任务不能一块儿使用。线程
若是一个程序每次最多只能获取一个锁,那么就不会产生锁顺序死锁。固然,这种状况一般并不现实。若是必须得到不少锁,那么在设计时必须考虑锁的顺序:尽可能减小潜在的加锁交互数量,将获取锁时须要遵循的协议写入文档并始终遵照。
在使用细粒度锁的程序中,能够经过使用一种两阶段策略(Two-Part Strategy)
来检查代码中的死锁:设计
有一项技术能够检测死锁和从死锁中恢复过来,即显式使用Lock类中的定时tryLock功能
来代替内置锁机制。当使用内置锁的时,只要没得到到锁,就会永远的等待下去,而显式锁则能够指定一个超时时限,在等待超过该时间后,tryLock会返回一个失败信息。若是超时时限比获取锁的时间长不少,那么能够在发生某个意外状况后从新得到控制权。
当定时锁失败时,你并不须要知道失败的缘由,或许是由于发生了死锁,或许是某个线程在持有锁时错误进入了无限循环,还有多是某个操做的执行时间远远超过了你的预期。然而至少你能记录所发生的失败,以及关于此次操做的其余有用信息,并经过一种更平缓的方式从新启动计算,而不是关闭整个进程。
即便在整个系统中没有始终使用定时锁,使用定时锁来获取多个锁也能有效的应对死锁问题。若是在获取锁时超时,那么就释放这个锁,而后后退一段时间后再次尝试,从而消除了发生死锁的条件,使程序恢复过来。(只有在同时获取两个锁时才有效,若是若是在嵌套的方法调用中请求多个锁,那么即便你知道已经持有了外层的锁,也没法释放它)。code
尽管死锁是最多见的活跃性危险,但在并发程序中还存在一些活跃性危险:饥饿
、丢失信号
、活锁
等。
当线程因为没法访问它所需的资源而不能执行时,就发生了饥饿(Starvation)
。引起饥饿的最多见资源就是CPU时钟周期,若是在java应用程序中对线程的优先级使用不当,或者在持有锁时执行一些没法结束的结构(例如无限循环、无限等待资源等),那么也有可能致使饥饿,由于其余须要这个锁的线程将没法获得它。
操做系统的线程调度器会尽力提供公平的,活跃性良好的调度,甚至远超出java语言规范的需求范围。在大多数java应用程序中,全部线程都具备相同的优先级Thread.NORMAL_PRIORITY。线程优先级并非直观的机制,提升了优先级之后可能起不到任何做用,也可能使得某个线程的调度优先于其余线程,从而致使饥饿。
咱们要尽可能避免使用线程优先级,由于这会增长平台依赖性,并可能致使活跃性问题。在绝大多数并发应用中,使用默认的线程优先级便可。
不良的锁管理可能致使糟糕的响应,例如某个线程长时间占有一个锁(或许正在对一个超大容器进行迭代,并对每一个元素进行计算密集的处理),从而使其余想要访问这个容器的线程就必须等待很长时间。
活锁(Livelock)
是另外一种形式的活跃性问题,该问题尽管不会阻塞线程,可是也不能继续执行,由于线程将不断重复执行相同的操做,并且老是会失败。活锁一般发生在处理事务消息的应用程序中:若是不能成功处理某个消息,那么消息处理机制将回滚整个事务,并将它从新放入这个队列的开头。再次执行到,又都会发出错误并回滚,所以处理器将被反复调用,虽然线程并无阻塞,可是也没法执行下去。这种活锁一般是由过分的错误恢复代码形成的:错误的将不可修复的错误做为可修复的错误。
要解决活锁问题,须要在重试机制中引入随机性。例如经过等待随机长度和回退能够有效地避免活锁发生。
活跃性问题是一个很是严重的问题,由于当出现活跃性问题时,除了停止应用程序以外没有其余任何机制能够帮助从这种故障中恢复。最多见的活跃性问题就是锁顺序死锁。在设计的时候应该要避免尝试锁顺序死锁,确保线程在获取多个锁的时候保持一致的顺序。若是状况容许,尽量多的使用开放调用。