死锁
锁顺序死锁
简单的锁顺序死锁示例:
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) {
synchronized (left) {
doSomethingElse();
}
}
}
void doSomething() {
}
void doSomethingElse() {
}
}
如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。
动态锁顺序死锁
下面的例子中锁的顺序通过传递给transferMoney参数的顺序
public static void transferMoney(Account fromAccount,
Account toAccount,
DollarAmount amount)
throws InsufficientFundsException {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
一个从x转向y,一个从y转向x。
1 A: transferMoney(myAccount, yourAccount, 10);
2 B: transferMoney(yourAccount, myAccount, 20);
解决办法:
通过System.identityHashCode(obj) 来定义锁的顺序。
小结:hashCode和identityHashCode的区别
String a = new String("hhh");
String b = new String("hhh");
打印发现对于String对象,只要a 和 b 的字符串是一样的,那么hashCode()方法返回的值必定相同,但是System.identityHashCode()方法不管什么情况下都不同。
- hashCode() 是根据 内容 来产生hash值的。
- System.identityHashCode() 是根据 内存地址 来产生hash值的。我们知道,new出来的String对象的内存地址是不一样的,所以hash值也不一样
解决办法:
public void transferMoney(final Account fromAcct,
final Account toAcct,
final DollarAmount amount)
throws InsufficientFundsException {
class Helper {
public void transfer() throws InsufficientFundsException {
if (fromAcct.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
}
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
if (fromHash < toHash) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized (toAcct) {
synchronized (fromAcct) {
new Helper().transfer();
}
}
} else {
//使用tie-breaking第三种锁来控制,在获得两个锁之前就要获得这个锁。
synchronized (tieLock) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}
协作对象之间发生死锁
如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
package net.jcip.examples;
import java.util.*;
import net.jcip.annotations.*;
/**
* CooperatingDeadlock
* <p/>
* Lock-ordering deadlock between cooperating objects
*
* @author Brian Goetz and Tim Peierls
*/
public class CooperatingDeadlock {
// Warning: deadlock-prone!
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination))
dispatcher.notifyAvailable(this);
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
class 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)
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
总结:正如在LeftRightDeadlock中发生的一样。
两个锁被两个线程以不同的顺序占有,产生死锁风险。
LeftRightDeadlock:
public void leftRight() {
synchronized (left) {
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
synchronized (right) {
synchronized (left) {
doSomethingElse();
}
}
}
setLocation和getImage:
//先获取taxis锁,然后获取Dispathcer锁。
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination))
//notifyAvailable也是一个加了锁的方法。
dispatcher.notifyAvailable(this);
}
//先获取了dispatcher锁,然后获取了每一个taxis锁。
public synchronized Image getImage() {
Image image = new Image();
for (Taxi t : taxis)
image.drawMarker(t.getLocation());
return image;
}
解决办法就是下一节说的开放调用。
开放调用——在调用某个方法时不需要持有锁
package net.jcip.examples;
import java.util.*;
import net.jcip.annotations.*;
/**
* CooperatingNoDeadlock
* <p/>
* Using open calls to avoiding deadlock between cooperating objects
*
* @author Brian Goetz and Tim Peierls
*/
class CooperatingNoDeadlock {
@ThreadSafe
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
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 (reachedDestination)
dispatcher.notifyAvailable(this);
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
@ThreadSafe
class 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);
}
//getImage不加锁
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;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
资源死锁——两个线程分别持有彼此想要的资源而又不会释放
例子:任务执行需要连接两个数据库,两个任务分别连接了其中一个数据库,而又等待彼此释放另一个数据库的资源
public class RenderPageTask implements Callable<String> {
public String call() throws Exception {
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBody();
// 出现死锁 -- task waiting for result of subtask
return header.get() + page + footer.get();
}
private String renderBody() {
// Here's where we would actually render the page
return "";
}
}
线程饥饿死锁——一个任务中提交另一个任务,并一直等待被提交任务完成
这些任务往往是产生线程饥饿死锁的主要来源,有界线程池 / 资源池与相互依赖的任务不能一起使用。
避免和诊断死锁
支持定时锁
显式使用Lock类中的定时tryLock功能来代替内置锁机制,显式锁则可以指定一个超时时限,在等待超过该时间后tryLock会返回一个失败信息。
通过线程转储信息来分析死锁
其他活跃性危险
饥饿
当线程访问他所需要的资源时却被永久的拒绝。以至于不能再继续执行。
Java中导致饥饿的原因:
- 高优先级线程吞噬所有的低优先级线程的CPU时间。
- 线程被永久堵塞在一个等待进入同步块的状态。
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法)。
在Java中实现公平性方案,需要:
- 使用锁,而不是同步块。
- 公平锁。
- 注意性能方面。
http://ifeve.com/starvation-and-fairness/
糟糕的响应性
活锁
活锁指线程一直处于运行状态,却在做无用的功,线程本身要完成的任务却一直无法进展。
活锁并不会阻塞,而是一直尝试去获取需要的锁,不断的try,这种情况下线程并没有阻塞而是一直活跃的状态,但是在做无用功。
就像两个人不断在自己的方向上让路。两个人不断从同一方向移动,但是却在做无用功。