避免活跃度危险
安全性和活跃度通常互相牵制。
使用锁来保证线程安全,但是滥用锁可能会引起锁顺序死锁。
使用线程池和信号量来约束资源的使用,但不知道那些管辖范围内的活动可能形成的资源死锁。
8.1死锁
死锁:当线程A占有锁L时,想要获得锁M,但是同时,线程B持有M,并尝试获得L,两个线程将永远等待下去。(发生在多个线程因为环路的锁依赖关系而永远等待的情况下)
数据库系统的设计就针对了检测死锁,以及从死锁中恢复。当数据库服务器监测到一个事务集发生了死锁,它会选择一个牺牲者,使它退出事务。
一个类如果有发生死锁的潜在可能并不意味着死锁每次都将发生,它只发生在该发生的情况下,当死锁出现的时候,往往是遇到了最糟糕的情况——高负载。
8.1.1锁顺序死锁
//这是一个锁顺序死锁例子//如果请求锁的顺序相同,就不会出现循环的锁依赖现象,也就不会产生死锁了。public class LeftRightDeadlock{private final Object left = new Object();private final Object right = new Object();public void leftRight(){synchronized(left){synchronized(right){doSomething();}}}public void rightLeft(){synchronized(right){synchronzied(left){doSomethingElse();}}}}
如果所有线程以通用的固定秩序获得锁,程序就不会出现锁顺序死锁问题了。
8.1.2动态的锁顺序死锁
//这个线程看起来都是通过同样的顺序获得锁的。
//事实上锁的顺序取决于传递给transferMoney的参数的顺序。
//如果两个线程同时调用,一个从X向Y转账,一个从Y向X转账,那么就会发生死锁
public class transferMoney(Account fromAccount,
Account toAccount,
DollarAmount amount){
synchronized(fromAccount){
synchronized(toAccount){
if(fromAccount.getBalance().compareTo(amount) < 0){
throw new InsufficientFundsException();
}else{
fromAccount,debit(amount);
toAccount.credit(amount);
}
}
}
}
因为参数的顺序是超出控制的,所以必须制定锁的顺序,并且在整个应用程序中,获得锁都必须始终遵守这个既定的顺序。
//通过对象的哈希值来定义锁的顺序。
//可能两个对象的哈希值相同,这时使用一个“加时赛”锁。但是这个锁可能会成为并发性的瓶颈。
private static final Object tieLock = new Object();
public void transferMoney(final Account fromAcct,
final Account toAcct,
final DollarAmount amount){
class Helper{
public void transfer(){
if (fromAcct.getBalance().compareTo(amount) < 0){
throw new Exception();
}else{
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
}
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identitHashCode(toAcct);
//通过对象的哈希值来制定锁顺序,避免顺序死锁
if(fromHash < toHash){
synchronized(fromAcct){
synchronized(toAcct){
new Helper().transfer();
}
}
}else if(fromHash > toHash){
synchronized(toAcct){
synchronized(fromAcct){
new Helper().transfer();
}
}
}else{//如果两个对象的哈希值相同,进入“加时赛”
synchronized(tieLock){
synchronized(fromAcct){
synchronized(toAcct){
new Helper().transfer();
}
}
}
}
}
8.1.3协作对象间的死锁
//当两个类同时执行时,当Taxi类运行到setLocation,Dispatcher运行到getImage时,会发生死锁。
class Taxi{
@GuardedBy("this")
private Point location, destination;
public Taxi(Dispatcher dispatcher){
this.dispatcher = dispatcher;
}
public synchronized Point getLocation(){
return location;
}
public synchronized void setLocation(Point location){
this.location = location;
if(location.equlas(destination))
//此时该线程已经拥有了Taxi的锁,尝试去获取Dispatcher的锁
dispatcher.notifyAvailable(this);
}
}
calss Dispatcher{
@GuardedBy("this")
private final Set<Taxi> taxis;
@GuardedBy("this")
private final Set<Taxi> availableTaxis;
public Dispatcher(){
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
public synchronized Image getImage(){
Image image = new Image();
for(Taxi t : taxis)
//此时该线程已经拥有了Dispatcher的锁,尝试去获取Taxi的锁
image.drawMarker(t.getLocation());
return image;
}
}
在持有锁的时候调用外部方法是在挑战活跃度问题,外部方法可能会获得其他锁(产生死锁的风险),或者遭遇严重超时的阻塞。当你持有锁的时候会延迟其他试图获得该锁的线程。
8.1.4开放调用
开放调用:调用的方法不需要持有锁。
尽可能的使用开放调用,这比获得多重锁后识别代码路径更简单,因此可以确保用一致的顺序获得锁。
//使用更小的synchronized块避免了死锁的风险
class Taxi{
@GuardedBy("this")
private Point location, destination;
public Taxi(Dispatcher dispatcher){
this.dispatcher = dispatcher;
}
public synchronized Point getLocation(){
return location;
}
public void setLocation(Point location){
boolean reachedDestination;
synchronized(this){
this.location = location;
reachedDestination = location.equals(destination);
}
if(location.equlas(reachedDestination))
dispatcher.notifyAvailable(this);
}
}
calss Dispatcher{
@GuardedBy("this")
private final Set<Taxi> taxis;
@GuardedBy("this")
private final Set<Taxi> availableTaxis;
public Dispatcher(){
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
public Image getImage(){
Set<Taxi> copy;
synchronized(this){
copy = new HashSet<Taxi>(taxis);
}
Image image = new Image();
for(Taxi t : copy)
image.drawMarker(t.getLocation());
return image;
}
}
在程序中尽量使用开放调用。依赖于开放调用的程序,相比于那些在持有锁的时候还可以调用外部方法的程序,更容易进行死锁自由度的分析。
8.1.5资源死锁
当线程相互间等待对方持有的锁,并且谁都不会释放自己的锁时就会发生死锁,当线程持有和等待的目标变为资源时,也会发生与之相类似的死锁。
加入你有两个放入池中的资源,比如分别是两个数据库的连接池,资源池通常是通过信号量实现的,当池为空时发生阻塞。如果线程A持有至数据库D1的连接,并等待连接到数据库D2,而线程B持有至D2的连接并等待到D1的连接。
另一种形式是基于资源的死锁是线程饥饿死锁。一个任务将工作提交到单线程化的Executor,并等待该Executor中的其他任务返回它的结果,在这种情况下,第一个任务将永远会等待下去,使这个任务以及所有在Executor中执行的其他任务永久停滞下来。
8.2避免和诊断死锁
在使用定义良好的锁的程序中,检测代码中死锁自由度的策略分为两个部分:
- 识别什么地方会获取多个锁(使这个集合尽量小),对这些示例进行全局分析,确保它们锁的顺序在程序中保持一致。
- 尽可能使用开放调用。
8.2.1尝试定时的锁
另一项监测死锁和从死锁中恢复的技术,是使用每个显式Lock类中定时tryLock特性,来替代使用内部锁机制。
在内部锁机制中,只要没有获得锁,就会永远保持等待,而显式的锁使你能够定义超时的时间,在规定时间之后tryLock还没有获得锁就返回失败。
当尝试获得定时锁失败时,你并不需要知道原因。不过至少有机会知道你的尝试已经失败,记录下有用的信息,并重新开始计算。
使用定时锁来获得多重锁能够有效应对死锁,如果获得锁的请求超时,可以释放这个锁,并后退,等待一会后再尝试。
8.2.2通过线程转储分析死锁
JVM使用线程转储来帮助我们识别死锁的发生。
线程转储包括每个运行中线程的栈追踪信息,以及与之相似并随之发生的异常,线程转储也包括锁的信息:哪个锁由哪个线程获得,获得这些锁的栈结构,阻塞线程正在等待哪个锁。
在生成线程转储之前,JVM在表示“正在等待”关系的有向图中搜索循环来寻找死锁。
在Unix平台,可以按下Ctrl-\键,在Windows平台按下 Ctrl-Break 键,来触发线程转储。很多IDE都要求线程转储。
如果使用了显式Lock类,线程转储获得的Lock的信息与内部锁提供的信息相比粗略了很多,内部锁与获得它的线程的栈框架相关联,显式Lock只会与获得它的线程相关联。
windows分析线程转储
1.选择已启动应用程序的命令行控制台窗口。
2.现在,在控制台窗口上,发出“ Ctrl + Break”命令。
3.这将生成线程转储。线程转储将被打印在控制台窗口本身上。
注1:在几台笔记本电脑(例如我的Lenovo T系列)中,“ Break”键被拔下。在这种情况下,您必须在Google上找到“ Break”的等效键。在我的情况下,事实证明“功能键+ B”等效于“断裂”键。因此,我不得不使用“ Ctrl + Fn + B”来生成线程转储。
注2:但是这种方法的一个缺点是线程转储将打印在Windows控制台本身上。如果不以文件格式获取线程转储,则很难使用线程转储分析工具(例如http://fasthread.io)。因此,当您从命令行启动应用程序时,将输出重定向到文本文件,即示例,如果您正在启动应用程序“ SampleThreadProgram”,则将发出以下命令:
java -classpath . SampleThreadProgram
而是像这样启动SampleThreadProgram
java -classpath . SampleThreadProgram > C:\workspace\threadDump.txt 2>&1
因此,当您发布“ Ctrl + Break”时,线程转储将发送到C:\ workspace \ threadDump.txt文件。
JVisualVM分析线程转储
启动程序后,右键进程,然后选择”线程Dump”
8.3其他的活跃度危险
死锁是最主要的活跃度危险,但仍可能会遇到一些其他的活跃度危险:饥饿、丢失信号、活锁。
8.3.1饥饿
饥饿:当线程访问它所需要的资源时却被永久拒绝,以至于不能再继续进行了。
最常见的引发饥饿的资源是CPU周期。
使用线程的优先级不当可能会引发饥饿,在锁中执行无终止构建也可能引起饥饿,因为其他需要这个锁的线程永远不可能得到它。
抵制使用线程优先级,它改变线程优先级的效果往往不明显,但是却让程序的行为变得与平台相关了,并且引入了饥饿发生的风险。
8.3.2弱响应性
主要发生在GUI应用程序中使用后台线程的情况下。
不良的锁管理也可能引起弱响应性,如果一个线程长时间占有一个锁(正在进行耗时的工作),其他想要访问该容器的线程就必须等待很长时间。
8.3.3活锁
活锁:线程中活跃度失败的一种形式,尽管没有被阻塞,线程却仍然不能继续,因为它不断重试相同的操作,却总是失败。
活锁通常发生在消息处理的应用程序中:
因为**消息处理失败**的话,其中**传递消息的底层框架会回退整个事务**,并把它**置回队首**,如果消息处理程序对某种特定类型的消息处理存在BUG,每次处理都会失败,那么它就会不停的重复。(这就是“毒药信息”问题)
活锁同样发生在多个相互协作的线程间,当它们为了彼此间响应而修改了状态,使得没有一个线程能够继续前进,那么就发生了活锁。
解决活锁的一种方案就是对重试机制引入一些随机性:
例如:在以太网络上,如果数据包发生了冲突,会通过使用一个随机组件进行当代重试。(如果都非常精准的在一秒后重试,那么就会不断的冲突下去)
在并发程序中,通过随机等待和撤回来进行重试能够相当有效地避免活锁的发生。
总结
活跃度失败是非常严重的问题,除了短时间地终止应用程序,没有任何机制可以恢复这种失败。
最常见的活跃度失败是:锁顺序死锁。
在设计时:确保多个线程在获得多个锁时,使用一致的顺序。
最好是在程序中使用开放调度,减少一个程序一次请求多个锁的情况,并且使这样的多重锁请求的发生更加明显。
