记得以前看过一个笑话:面试官问死锁怎么回事儿?我想了想,然后回答面试官说你先发offer给我,我再回答你。这就是现实中的死锁。
代码中的死锁是当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。举个简单的例子,线程1先持有了锁A,准备再请求锁B,但是在这之前线程2持有了锁B准备请求锁A,这时双方就会造成死锁。
上面这个简化过的例子只能用于理解死锁的形成,对于解决死锁而言参考价值有限。现实的代码中,锁的方式多种多样有方法上的synchronized
、ReentrantLock
等等,当他们混在一起用的时候,解决起来就比较棘手了。比如下面一些例子:
public void testLock() throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
reentrantLock.unlock();
ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(16);
integers.take();
Semaphore semaphore = new Semaphore(1);
semaphore.acquire();
semaphore.release();
}
public synchronized void process() { //lock on `this`
}
public static synchronized void processStatic() { //lock on class
}
很多时候要直接从代码逻辑上找到死锁是很困难的。必须要实际遇到死锁并借助一些工具才能解决这个问题。
- 线程dump
使用jstack <process id>
或者kill -3 <process id>
image.png
可以看到Found one Java-level deadlock:
和下面输出的详细信息。通过这些信息可以辅助解决死锁问题。 - 通过Java SDK提供的JVM死锁检查api:
public void detectDeadlock() {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
boolean findDeadlock = deadlockedThreads != null && deadlockedThreads.length > 0;
System.out.println("Find deadlock:" + findDeadlock);
}
当然为了能及时检查出死锁,需要开启一个后台线程并进行合适频率的轮询。
上面的方法是在出现死锁时的辅助检查手段,有没有什么方法可以尽量在代码中避免出现死锁呢?有,获取锁的时候增加超时限制。
public void lockWithTimeout() throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
boolean acquire = reentrantLock.tryLock(2, TimeUnit.MINUTES);
ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(16);
integers.poll(2, TimeUnit.MINUTES);
Semaphore semaphore = new Semaphore(1);
semaphore.tryAcquire(2, TimeUnit.MINUTES);
}
OK,让我们再来看一个例子,下面的代码是否会形成死锁呢?
public void doTransfer() {
Account account1 = new Account();
Account account2 = new Account();
//线程1
new Thread(() -> transfer(account1, account2, 100)).start();
//线程2
new Thread(() -> transfer(account2, account1, 200)).start();//注意和上面的传参顺序相反
}
/**
* 从account2转账至account1
*
* @param account1
* @param account2
* @param amount
*/
public void transfer(Account account1, Account account2, int amount) {
synchronized (account1) {
synchronized (account2) {
account1.add(amount);
account2.reduce(amount);
}
}
}
class Account {
public void add(int amount) {
}
public void reduce(int amount) {
}
}
这样是会形成死锁的。线程1如果刚走到synchronized (account1)
这里,而线程2也走到synchronized (account1)
这里,由于account1的实际传入对象分别为account1
和account2
,这时再向下走就会形成死锁。那怎么改上面的代码就可以避免死锁呢?下面是修改过的代码:
public void doTransfer() {
Account account1 = new Account();
Account account2 = new Account();
new Thread(() -> transfer(account1, account2, 100)).start();
new Thread(() -> transfer(account2, account1, 200)).start();
}
/**
* 从account2转账至account1
*
* @param account1
* @param account2
* @param amount
*/
public void transfer(Account account1, Account account2, int amount) {
//对account1和account2排序,使无论account1和account2如何传入在加锁时顺序一致
Account firstAccount = getFirstAccount(account1, account2);
Account secondAccount = getSecondAccount(account1, account2);
synchronized (firstAccount) {
synchronized (secondAccount) {
account1.add(amount);
account2.reduce(amount);
}
}
}
private Account getFirstAccount(Account account1, Account account2) {
boolean b = account1.getId() > account2.getId();//仅作示例,表示account1某种固定属性可以和account2比较
if (b) {
return account1;
} else {
return account2;
}
}
private Account getSecondAccount(Account account1, Account account2) {
boolean b = account1.getId() < account2.getId();//仅作示例,表示account1某种固定属性可以和account2比较
if (b) {
return account1;
} else {
return account2;
}
}
class Account {
public void add(int amount) {
}
public void reduce(int amount) {
}
public int getId() {
return 1;
}
}
通过保持传入对象加锁顺序一致就可避免死锁问题。
网友评论