文章目录
死锁
死锁出现的情况
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) {
synchronized (left) {
doSomethingElse();
}
}
}
2. 动态的锁顺序死锁
还是从A账户转钱到B账户的例子。
public void transferMoney(Account fromAcct,
Account toAcct,
DollarAmount amount)
throws InsufficientFundsException {
synchronized (fromAcct) {
synchronized (toAcct) {
if (fromAcct.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
//减去钱
fromAcct.debit(amount);
//增加钱
toAcct.credit(amount);
}
}
}
}
解决方案
看似所有的代码都按照 from 到 to 的方式加锁,但问题在于from和to依赖于调用者,完全存在同时调用了transferMoney(A, B)和transferMoney(B, A)的可能性。改进的措施就是将无序变有序
(不管调用者如何调用,在整个程序执行期间,内部执行的顺序都将固定)
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();
}
}
}
}
}
提取可用来比较的 hashCode 来决定顺序,如果极端情况下出现相同,使用加时赛(Tie-Breaking)锁来保证同时只有一个线程以未知的顺序加锁。
当然,在真实和开发过程中,如果存在唯一的数据值,那么直接比较整个唯一的数据值来决定加锁的顺序即可。
3. 在协作对象之间发生的死锁
此类死锁不像之前那么明显,因为其两个锁的出现不一定在同一个方法里。(这种较难辨别)
。考虑下面两个相互协作的类,在出租车调度系统中可能会用到它们。Taxi代表一个出租车对象,包含位置和目的地两个属性,Dispatcher 代表一个出租车车队。
public class CooperatingDeadlock {
// 容易发生死锁
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;
}
//会获得 Taxi 和 dispatcher 的锁
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);
}
// 会获得 dispatcher 和 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) {
}
}
}
如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
解决方案----开放调用
看上面的文字,如果我们在调用某个外部方法时,自身不持有锁,是不是就会避免这种情况呐?这种就叫开放调用!
@ThreadSafe
class Taxi {
@GuardedBy("this") private Point location, destination;
private final 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);
}
}
@ThreadSafe
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") private final Set<Taxi> availableTaxis;
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;
}
}
在程序中应尽量使用开放调用。与那些在持有锁时调用外部方法的程序相比,更易于对依赖于开放调用的程序进行死锁分析。
4.资源死锁
- 资源占用的死锁
- 线程饥饿死锁:例子:单线程 Executor,等待的资源在工作队列中。
死锁的避免与诊断
- 避免一个线程同时获取多个锁;
- 确保锁的顺序全局一致,尽量减少锁的交互,使用开放调用
- 避免一个线程在锁内占有多个资源,尽量保证每个锁只占有一个资源;
- 使用定时锁,即 lock.tryLock(timeout),这样拿不到锁就放弃,不会发生死锁一直卡在那里;
- 对于数据库锁,加锁和解锁必须在同一个数据库连接中,否则可能会解锁失败。
诊断
- 通过线程转储来分析
其他的活跃性危险
饥饿
- 线程由于无法访问它所需要的资源而不能继续执行。
- 最常见资源就是 CPU 时钟周期 。如果在 Java 应用程序中对线程的优先级使用不当,或者在持有锁时执行一些无法结束的结构(例如无限循环,或者无限制地等待某个资源),那么也可能导致饥饿,因为其他需要这个锁的线程将无法得到它。
尽量不要改变线程的优先级 。只要改变了线程的优先级,程序的行为就将与平台相关,并且会导致发生饥饿问题的风险。你经常能发现某个程序会在一些奇怪的地方调用 Thread.sleep 或 Thread.yield,这是因为该程序试图克服优先级调整问题或响应性问题,试图让低优先级的线程执行更多的时间。
活锁(处理逻辑上的一种“死锁”)
- 当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个过于礼貌的人在半路上面对面地相遇:他们彼此都让出对方的路,然而又在另一条路上相遇了,因此他们就这样反复地避让下去。
- 解决方法: 在重试机制中引入随机性。