死锁,一组互相竞争的资源的线程之间相互等待,导致永久阻塞的现象。
如下图所示:
与死锁对应的,还有活锁,是指线程没有出现阻塞,但是无限循环。
有一个经典的银行转账例子如下:
我们有个账户类,其中有两个属性:账户名和余额。
它有两个方法:转入、转出。
public class Account {
private String countName;
private int balance;
public Account(String countName, int balance) {
this.countName = countName;
this.balance = balance;
}
/**
* 转出金额,更新转出方余额,金额减少
* @param amount
*/
public void debit(int amount){
this.balance -= amount;
}
/**
* 存入金额,更新转入方余额,金额增多
* @param amount
*/
public void credit(int amount){
this.balance += amount;
}
public String getCountName() {
return countName;
}
public void setCountName(String countName) {
this.countName = countName;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
还有一个转账操作类,它有三个属性:转入账户、转出账户、转账金额。
它实现了Runnable接口,run方法中是转账逻辑代码。
我们使用 while(true) 让他不停的进行转账操作:如果账户余额大于转账金额,就让转出账号减少amount,转入账户增加amount。打印出线程名、金额转移方向、以及每个账户余额。
我们在main方法中进行测试,创建两个转账账户,使用两个线程操作这两个账户,让他们相互转账。
public class TransferAccount implements Runnable{
private Account fromAccount;
private Account toAccount;
private int amount;
public TransferAccount(Account fromAccount, Account toAccount, int amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run() {
while(true){
synchronized (fromAccount){
synchronized (toAccount){
if(fromAccount.getBalance()>=amount) {
fromAccount.debit(amount);
toAccount.credit(amount);
}
System.out.println(Thread.currentThread().getName());
System.out.println(fromAccount.getCountName()+"->"+toAccount.getCountName()+":"+amount );
System.out.println(fromAccount.getCountName() +"账户余额:"+ fromAccount.getBalance());
System.out.println(toAccount.getCountName() +"账户余额:"+ toAccount.getBalance());
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Account bigHead = new Account("冤大头",100000);
Account smallKill = new Account("门小抠",200000);
new Thread(new TransferAccount(bigHead,smallKill,10)).start();
new Thread(new TransferAccount(smallKill,bigHead,20)).start();
}
}
执行main方法后的输出结果:
Thread-0
冤大头->门小抠:10
冤大头账户余额:99990
门小抠账户余额:200010
Thread-1
门小抠->冤大头:20
门小抠账户余额:199990
冤大头账户余额:100010
Thread-1
门小抠->冤大头:20
门小抠账户余额:199970
冤大头账户余额:100030
Thread-0
冤大头->门小抠:10
冤大头账户余额:100020
门小抠账户余额:199980
Thread-0
冤大头->门小抠:10
冤大头账户余额:100010
门小抠账户余额:199990
Thread-1
门小抠->冤大头:20
门小抠账户余额:199970
冤大头账户余额:100030
Thread-0
冤大头->门小抠:10
冤大头账户余额:100020
门小抠账户余额:199980
Thread-1
门小抠->冤大头:20
门小抠账户余额:199960
冤大头账户余额:100040
Thread-0
冤大头->门小抠:10
冤大头账户余额:100030
门小抠账户余额:199970
Thread-1
门小抠->冤大头:20
门小抠账户余额:199950
冤大头账户余额:100050
Thread-0
冤大头->门小抠:10
冤大头账户余额:100040
门小抠账户余额:199960
Thread-1
门小抠->冤大头:20
门小抠账户余额:199940
冤大头账户余额:100060
这时,我们发现进程还在运行,但是控台停止输出了,它停在那里了。
截图如下:
这时我们使用 jps 来查看一下java进程:
D:\open_source\MyBatis\MyThread\target\classes\demo>jps
23600
24676 Launcher
40244 Jps
23672 TransferAccount
然后输入 使用jstack来看详情:
D:\open_source\MyBatis\MyThread\target\classes\demo>jstack 23672
2023-02-26 18:37:57
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.351-b10 mixed mode):
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x000001b4fc785800 nid=0x6530 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #13 prio=5 os_prio=0 tid=0x000001b4fc77f800 nid=0x887c waiting for monitor entry [0x000000467adff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at deadlock.TransferAccount.run(TransferAccount.java:20)
- waiting to lock <0x000000076c5a4610> (a deadlock.Account)
- locked <0x000000076c5a4658> (a deadlock.Account)
at java.lang.Thread.run(Thread.java:750)
"Thread-0" #12 prio=5 os_prio=0 tid=0x000001b4fc77c800 nid=0x8c80 waiting for monitor entry [0x000000467acff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at deadlock.TransferAccount.run(TransferAccount.java:20)
- waiting to lock <0x000000076c5a4658> (a deadlock.Account)
- locked <0x000000076c5a4610> (a deadlock.Account)
at java.lang.Thread.run(Thread.java:750)
......
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000001b4fa208eb8 (object 0x000000076c5a4610, a deadlock.Account),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000001b4fa208e08 (object 0x000000076c5a4658, a deadlock.Account),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at deadlock.TransferAccount.run(TransferAccount.java:20)
- waiting to lock <0x000000076c5a4610> (a deadlock.Account)
- locked <0x000000076c5a4658> (a deadlock.Account)
at java.lang.Thread.run(Thread.java:750)
"Thread-0":
at deadlock.TransferAccount.run(TransferAccount.java:20)
- waiting to lock <0x000000076c5a4658> (a deadlock.Account)
- locked <0x000000076c5a4610> (a deadlock.Account)
at java.lang.Thread.run(Thread.java:750)
Found 1 deadlock.
我们可以看到,其中Thread-0和Thread-1都已经处于BLOCK状态,发生了死锁。
导致死锁发生,必须同时满足四个条件:互斥、占有且等待、不可抢占、循环等待。
- 互斥:共享资源A和B只能被一个线程占用。
- 占有且等待:线程Thread-1 已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X。
- 不可抢占:其他线程不能强行抢占线程Thread-1 占有的资源。
- 循环等待:线程Thread-1 等待线程Thread-2 占有的资源,线程Thread-2等待Thread-1 占有的资源。
如何解决?
如果要解决死锁问题,只需要破坏其中一个条件,使其不满足即可。
除了互斥(多线程的基础)之外,其他三个条件都可以考虑破坏。
实际中遇到死锁,只能重启,如果还是死锁,要定位问题点,然后修复代码(破坏掉死锁必须满足的条件之一)后重新发布。
我们来尝试破坏占有且等待的方式来破坏死锁:
新增加一个分配的类:Allocator,它有一个账户池。每个转账线程中要判断,账户池中是否已有此账户,如果没有可以转账,有的话就不转账。就可以避免共享资源同时存在于两个线程。
import java.util.ArrayList;
import java.util.List;
public class Allocator {
private List<Object> list = new ArrayList<>();
synchronized boolean apply(Object fromAccount,Object toAccount){
if(list.contains(fromAccount)||list.contains(toAccount)){
return false;
}
list.add(fromAccount);
list.add(toAccount);
return true;
}
synchronized void free(Object fromAccount,Object toAccount){
list.remove(fromAccount);
list.remove(toAccount);
}
}
//相应的,TransAcount也要做修改:
public class TransferAccount implements Runnable {
private Account fromAccount;
private Account toAccount;
private int amount;
private Allocator allocator;
public TransferAccount(Account fromAccount, Account toAccount, int amount, Allocator allocator) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
this.allocator = allocator;
}
@Override
public void run() {
while (true) {
//判断账户是否已经分配过
if (allocator.apply(fromAccount, toAccount)) {
try {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance() >= amount) {
fromAccount.debit(amount);
toAccount.credit(amount);
}
System.out.println(Thread.currentThread().getName());
System.out.println(fromAccount.getCountName() + "->" + toAccount.getCountName() + ":" + amount);
System.out.println(fromAccount.getCountName() + "账户余额:" + fromAccount.getBalance());
System.out.println(toAccount.getCountName() + "账户余额:" + toAccount.getBalance());
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
allocator.free(fromAccount,toAccount);
}
}
}
}
public static void main(String[] args) {
Account bigHead = new Account("冤大头", 100000);
Account smallKill = new Account("门小抠", 200000);
Allocator allocator = new Allocator();
new Thread(new TransferAccount(bigHead, smallKill, 10, allocator)).start();
new Thread(new TransferAccount(smallKill, bigHead, 20, allocator)).start();
}
}
我们来避免第三个条件:使用Lock来替换掉 Synchronized。
Synchronized加锁后,要等到资源释放,而且锁是不可抢占的。
Lock中有个方法 tryLock,可以返回布尔值,如果返回fasle就不会进去。tryLock不会持续持有锁。
public class TransferAccount2 implements Runnable{
private Account fromAccount;
private Account toAccount;
private int amount;
private Lock fromAccountLock = new ReentrantLock();
private Lock toAccountLock = new ReentrantLock();
public TransferAccount2(Account fromAccount, Account toAccount, int amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run() {
while(true){
//synchronized (fromAccount){
if(fromAccountLock.tryLock()){
if(toAccountLock.tryLock()){
if(fromAccount.getBalance()>=amount) {
fromAccount.debit(amount);
toAccount.credit(amount);
}
System.out.println(Thread.currentThread().getName());
System.out.println(fromAccount.getCountName()+"->"+toAccount.getCountName()+":"+amount );
System.out.println(fromAccount.getCountName() +"账户余额:"+ fromAccount.getBalance());
System.out.println(toAccount.getCountName() +"账户余额:"+ toAccount.getBalance());
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Account bigHead = new Account("冤大头",100000);
Account smallKill = new Account("门小抠",200000);
new Thread(new TransferAccount2(bigHead,smallKill,10)).start();
new Thread(new TransferAccount2(smallKill,bigHead,20)).start();
}
}