Java 死锁分类
1. 死锁简介
经典的“哲学家进餐”问题很好的描述了死锁的情况。5个哲学家吃中餐,坐在一张圆桌上,有5根筷子,每个人吃饭必须用两根筷子。哲学家时而思考时而进餐。分配策略有可能导致哲学家永远无法进餐。
类似的,当线程A尝试持有锁L1,并尝试获取锁L2;同时,线程B持有锁L2,并尝试获取锁L1,并且都不释放已经拥有的锁。这就是最简单的死锁。其中存在环状的锁依赖关系。称为“抱死”。
数据库系统有监视、检测死锁的环节。当两个事务需要的锁相互依赖时,DB将选择一个牺牲者放弃这个事务,牺牲者会释放持有的资源,从而使其他事务顺利的执行。
JVM在解决死锁问题时并没有数据库系统那么强大,当一组线程发生死锁时,那么这写线程就凉凉——永远不会被使用。
2. 锁顺序死锁
经典案例是LeftRightDeadlock,两个方法,分别是leftRigth、rightLeft。如果一个线程调用leftRight,另一个线程调用rightLeft,且两个线程是交替执行的,就会发生死锁。
public class LeftRightDeadLock {
private static Object left = new Object();
private static Object right = new Object();
public static void leftRigth() {
synchronized (left) {
System.out.println("leftRigth: left lock");
synchronized (right) {
System.out.println("leftRigth: right lock");
}
}
}
public static void rightLeft() {
synchronized (right) {
System.out.println("rightLeft: right lock");
synchronized (left) {
System.out.println("rightLeft: left lock");
}
}
}
public static void main(String[] args) {
new Thread(() -> {
for (int i=0; i<100; i++) {
leftRigth();
}
}).start();
new Thread(() -> {
for (int i=0; i<100; i++) {
rightLeft();
}
}).start();
}
}
3. 动态的锁顺序死锁
上例告诉我们,交替的获取锁会导致死锁,且锁是固定的。有时候并锁的执行顺序并不那么清晰,参数导致不同的执行顺序。经典案例是银行账户转账,from账户向to账户转账,在转账之前先获取两个账户的锁,然后开始转账,如果这是to账户向from账户转账,角色互换,也会导致锁顺序死锁。
@Slf4j
public class TransferMoneyDeadlock {
public static void transfer(Account from, Account to, int amount) throws Exception {
synchronized (from) {
log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), from.name);
synchronized (to) {
log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), to.name);
if (from.balance < amount) {
throw new Exception("余额不足");
} else {
from.debit(amount);
to.credit(amount);
log.info("线程【{}】从【{}】账户转账到【{}】账户【{}】元钱成功", Thread.currentThread().getName(), from.name, to.name, amount);
}
}
}
}
private static class Account {
String name;
int balance;
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
void debit(int amount) {
this.balance = balance - amount;
}
void credit(int amount) {
this.balance = balance + amount;
}
}
public static void main(String[] args) {
Account A = new Account("A", 100);
Account B = new Account("B", 200);
Thread t1 = new Thread(() -> {
for (int i=0; i<100; i++) {
try {
transfer(A, B, 1);
} catch (Exception e) {
e.printStackTrace();
}
}
});
t1.setName("T1");
Thread t2 = new Thread(() -> {
for (int i=0; i<100; i++) {
try {
transfer(B, A, 1);
} catch (Exception e) {
e.printStackTrace();
}
}
});
t2.setName("T2");
t1.start();
t2.start();
}
}
单单看transfer方法看不出来有leftRight锁顺序死锁,多线程调用该方法后,出现两个账户同时转账的情况,同样引起了锁顺序死锁。下面通过确定两个账户的顺序解决该问题。
@Slf4j
public class TransferMoneyOrdered {
private static Object commObj = new Object();
public static void transfer(Account from, Account to, int amount) throws Exception {
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash > toHash) {
synchronized (from) {
log.info("线程: 【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), from.name);
synchronized (to) {
log.info("线程: 【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), to.name);
new Helper().transfer(from, to, amount);
}
}
} else if (fromHash < toHash) {
synchronized (to) {
log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), to.name);
synchronized (from) {
log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), from.name);
new Helper().transfer(from, to, amount);
}
}
} else {
synchronized (commObj) {
synchronized (from) {
log.info("线程 【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), from.name);
synchronized (to) {
log.info("线程【{}】获取【{}】账户锁成功...", Thread.currentThread().getName(), to.name);
new Helper().transfer(from, to, amount);
}
}
}
}
}
private static class Helper {
public void transfer(Account from, Account to, int amount) throws Exception {
if (from.balance < amount) {
throw new Exception("余额不足");
} else {
from.debit(amount);
to.credit(amount);
log.info("线程:【{}】从【{}】账户转账到【{}】账户【{}】元钱成功", Thread.currentThread().getName(), from.name, to.name, amount);
}
}
}
private static class Account {
String name;
int balance;
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
void debit(int amount) {
this.balance = balance - amount;
}
void credit(int amount) {
this.balance = balance + amount;
}
}
public static void main(String[] args) {
Account A = new Account("A", 100);
Account B = new Account("B", 200);
Thread t1 = new Thread(() -> {
for (int i=0; i<100; i++) {
try {
transfer(A, B, 1);
} catch (Exception e) {
e.printStackTrace();
}
}
});
t1.setName("T1");
Thread t2 = new Thread(() -> {
for (int i=0; i<100; i++) {
try {
transfer(B, A, 1);
} catch (Exception e) {
e.printStackTrace();
}
}
});
t2.setName("T2");
t1.start();
t2.start();
}
}
如上,通过账户的hash取值确保锁顺序的一致性,从而避免两个线程交替获取两个账户的锁。
4. 在协作对象之间发生的死锁
上述两例中,在同一个方法中获取两个锁。实际上,锁并不一定在同一方法中被获取。经典案例,如出租车调度系统。
@Slf4j
public class CooperateCallDeadlock {
private static class Taxi {
private String location;
private String destination;
private Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher, String destination) {
this.dispatcher = dispatcher;
this.destination = destination;
}
public synchronized String getLocation() {
log.info("获取锁Taxi:getLocation");
return location;
}
public synchronized void setLocation(String location) {
log.info("获取锁Taxi:setLocation");
this.location = location;
if (location.equals(destination)) {
dispatcher.notifyAvailable(this);
}
}
}
private static class Dispatcher {
private Set<Taxi> availableTaxis;
public Dispatcher() {
availableTaxis = new HashSet<>();
}
public synchronized void notifyAvailable(Taxi taxi) {
log.info("获取锁Dispatcher:notifyAvailable");
availableTaxis.add(taxi);
}
public synchronized void driveTaxis() {
log.info("获取锁Dispatcher:driveTaxis");
for (Taxi t : availableTaxis) {
log.info("开到:【{}】", t.getLocation());
}
}
}
public static void main(String[] args) {
Dispatcher dispatcher = new Dispatcher();
Taxi A = new Taxi(dispatcher, "LA");
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
A.setLocation("LA");
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
dispatcher.driveTaxis();
}
}).start();
}
}
上例代码中,没有一个方法显示获取两个锁,但是方法调取需要再次获得锁,setLocation和driveTaxis本身是同步方法,在方法体内又需要获取锁,这样交替执行导致了锁顺序死锁。在LeftRightDeadlock、TransferMoneyDeadlock中,要查找死锁比较简单,只需要找到方法体内获取两个锁的地方。而出租车调度的案例中查找死锁比较困难,警惕在已经持有锁的方法内调用外部同步方法。
解决办法就是使用开放调用,开放调用指调用该方法不需要持有锁。
@Slf4j
public class CooperateCallOpened {
private static class Taxi {
private String location;
private String destination;
private Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher, String destination) {
this.dispatcher = dispatcher;
this.destination = destination;
}
public synchronized String getLocation() {
log.info("获取锁Taxi:getLocation");
return location;
}
public void setLocation(String location) {
boolean flag = false;
synchronized (this) {
log.info("获取锁Taxi:setLocation");
this.location = location;
if (location.equals(destination)) {
flag = true;
}
}
if (flag) {
dispatcher.notifyAvailable(this);
}
}
}
private static class Dispatcher {
private Set<Taxi> availableTaxis;
public Dispatcher() {
availableTaxis = new HashSet<>();
}
public synchronized void notifyAvailable(Taxi taxi) {
log.info("获取锁Dispatcher:notifyAvailable");
availableTaxis.add(taxi);
}
public void driveTaxis() {
Set<Taxi> copy;
synchronized (this) {
log.info("获取锁Dispatcher:driveTaxis");
copy = new HashSet<>(availableTaxis);
}
for (Taxi t : copy) {
log.info("开到:【{}】", t.getLocation());
}
}
}
public static void main(String[] args) {
Dispatcher dispatcher = new Dispatcher();
Taxi A = new Taxi(dispatcher, "LA");
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
A.setLocation("LA");
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
dispatcher.driveTaxis();
}
}).start();
}
}