主要讲解Java中常见的锁。
前言
并发编程系列应该快接近尾声,锁可能是这个系列的最后一篇,重要的基本知识应该都涵盖了。然后对于书籍《Java并发编程实战》,最后面的几章,我也只看了锁的部分,这篇文章主要是对该书中锁的内容进行一个简单的总结。
死锁
死锁是指一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
锁顺序死锁
我们先看一个死锁的示例,我们先定义个BankAccount对象,来存储基本信息,代码如下:
public class BankAccount {
private int id;
private double balance;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
接下来,我们使用细粒度锁来尝试完成转账操作:
public class BankTransferDemo {
public void transfer(BankAccount sourceAccount, BankAccount targetAccount, double amount) {
synchronized(sourceAccount) {
synchronized(targetAccount) {
if (sourceAccount.getBalance() > amount) {
System.out.println("Start transfer.");
sourceAccount.setBalance(sourceAccount.getBalance() - amount);
targetAccount.setBalance(targetAccount.getBalance() + amount);
}
}
}
}
}
如果进行下述调用,就会产生死锁:
transfer(myAccount, yourAccount, 10);
transfer(yourAccount, myAccount, 10);
如果执行顺序不当,那么A可能获取myAccount的锁并等待yourAccount的锁,然而B此时持有yourAccount的锁,并正在等待myAccount的锁。
通过顺序来避免死锁
由于我们无法控制参数的顺序,如果要解决这个问题,必须定义锁的顺序,并在整个应用程序中按照这个顺序来获取锁。我们可以通过Object.hashCode返回的值,来定义锁的顺序:
public class BankTransferDemo {
private static final Object tieLock = new Object();
public void transfer(BankAccount sourceAccount, BankAccount targetAccount, double amount) {
int sourceHash = System.identityHashCode(sourceAccount);
int targetHash = System.identityHashCode(targetAccount);
if (sourceHash < targetHash) {
synchronized(sourceAccount) {
synchronized(targetAccount) {
if (sourceAccount.getBalance() > amount) {
sourceAccount.setBalance(sourceAccount.getBalance() - amount);
targetAccount.setBalance(targetAccount.getBalance() + amount);
}
}
}
} else if (sourceHash > targetHash) {
synchronized(targetAccount) {
synchronized(sourceAccount) {
if (sourceAccount.getBalance() > amount) {
sourceAccount.setBalance(sourceAccount.getBalance() - amount);
targetAccount.setBalance(targetAccount.getBalance() + amount);
}
}
}
} else {
synchronized (tieLock) {
synchronized(targetAccount) {
synchronized(sourceAccount) {
if (sourceAccount.getBalance() > amount) {
sourceAccount.setBalance(sourceAccount.getBalance() - amount);
targetAccount.setBalance(targetAccount.getBalance() + amount);
}
}
}
}
}
}
}
无论你入参怎么变化,通过hash值的大小,我们永远是先锁住hash值小的数据,再锁hash值大的数据,这样就保证的锁的顺序。
但是在极少数情况下,两个对象的Hash值相同,如果顺序错了,仍可能导致死锁,所以在获取两个锁之前,使用“加时赛(Tie-Breaking)”锁,保证每次只有一个线程以未知的顺序获取到该锁。但是如果程序经常出现Hash冲突的情况,这里会成为并发的瓶颈,因为final变量是内存可见,会让所有的线程都阻塞到该锁上,不过这种概率会很低。
在协作对象之间发生死锁
这里我就只简单说明一下,就是有两个对象A和B,A.action_A1()会调用B中的方法action_B1(),同时B.action_B2()会调用A中的方法action_A2(),由于这四个方法action_A1()、action_A2()、action_B1()、action_B2()都通过synchronized加锁,我们知道都通过synchronized在方法上加的是对象锁,所以可能存在A调用B的方法时,B也正在调用A的方法,导致互相等待出现死锁的情况。
具体的示例,大家可以参考《Java并发编程实战》书籍第174页的内容。
ReentrantLock
使用方法
在协调对象的访问时可以使用的机制只有synchronized和volatile,Java 5.0增加了一种新的机制:ReentrantLock。ReentrantLock并不是一种替代内置锁的方法,而是当内置锁机制不适用时,作为一种可选的高级功能。
下面看一个简单的示例:
Lock lock = new ReentrantLock();
//...
lock.lock();
try {
// ...
} finally {
lock.unlock();
}
除了上述不可替换synchronized的原因,就是需要手动通过lock.unlock()释放该锁,如果忘记释放,那将是个非常严重的问题。
通过tryLock避免顺序死锁
还是沿用上面的死锁示例,我们通过tryLock()进行简单改造:
回复