线程死锁
什么是死锁
线程1持有资源A,线程2持有资源B。这时如果线程1去请求资源B,线程2去请求资源A。由于资源A和资源B都已经被其它线程所持有,导致线程1和线程2一直无法获取到想要的资源,陷入无限等待状态。
一个必然死锁的程序
下面通过一段代码来演示下:
/**
* 必然死锁的例子
*/
public class MustDeadLock implements Runnable {
int state = 1;
static Object lock1 = new Object();
static Object lock2 = new Object();
public static void main(String[] args) {
MustDeadLock mustDeadLock = new MustDeadLock();
MustDeadLock mustDeadLock1 = new MustDeadLock();
mustDeadLock.state = 0;
new Thread(mustDeadLock).start();
new Thread(mustDeadLock1).start();
}
@Override
public void run() {
try {
if (state == 1) {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName()+"获取到lock1");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+"准备获取lock2");
synchronized (lock2) {
System.out.println(Thread.currentThread().getName()+"获取到lock2");
}
}
}
if (state == 0) {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName()+"获取到lock2");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+"准备获取lock1");
synchronized (lock1) {
System.out.println(Thread.currentThread().getName()+"获取到lock1");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
Thread-0获取到lock2
Thread-1获取到lock1
Thread-1准备获取lock2
Thread-0准备获取lock1
Thread-0获取到lock2,Thread-1获取到lock1。这时当Thread-1准备获取lock2时,由lock2被Thread-0持有,所以Thread-1就陷入了无限等待状态。Thread-0也一样一直等待获取lock1。
死锁发生的四个必要条件
通过以上例子,我们分析下死锁发生的四个必要条件。
1:互斥条件。无论时lock1 还是lock2,都只能同时被一个线程所持有。
2:持有并请求。Thread-0持有了一个资源lock2,这时它又去请求另外一个资源。
3:不剥夺条件。Thread-0持有的资源lock2只能由它自己去释放,线程不能释放不是由自己持有的资源。
4:环路等待。Thread-0获取到lock2,去请求lock1,Thread-1获取到lock1,去请求lock2。Thread-0在等待Thread-1持有的资源,Thread-1在等待Thread-0持有的资源。Thread-0 --》Thread-1 --》Thread-0。
如果发生死锁,以上这四个条件缺一不可。
实际生产中死锁问题
银行转账问题
有两个用户张三和李四,张三的账户余额有500元,李四的账户余额有500元。在同一时间张三向李四的账户转200元,李四向张三的账户转200元,互相转了100次。以下是具体实现。
/**
* 模拟两个人转账导致线程死锁问题
*/
public class TransferMoney implements Runnable {
Acount a;
Acount b;
int amount;
public TransferMoney(Acount a, Acount b, int amount) {
this.a = a;
this.b = b;
this.amount = amount;
}
public static void main(String[] args) {
Acount acount = new Acount(500);
Acount acount1 = new Acount(500);
TransferMoney transferMoney = new TransferMoney(acount, acount1, 200);
TransferMoney transferMoney2 = new TransferMoney(acount1, acount, 200);
for (int i = 0; i < 100; i++) {
new Thread(transferMoney).start();
new Thread(transferMoney2).start();
}
}
@Override
public void run() {
try {
transfer(a, b, amount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void transfer(Acount a, Acount b, int amount) throws InterruptedException {
synchronized (a) {
synchronized (b) {
if (a.money < amount) {
System.out.println("账户余额不足,转账失败");
}
a.money -= amount;
b.money += amount;
System.out.println("转账成功");
}
}
}
static class Acount {
int money;
public Acount(int money) {
this.money = money;
}
}
}
执行结果:
互相转账100次,总共应该打印200次转账结果。但只执行了两次程序就没有执行下去了。下面分析下导致这个问题的原因。
张三向李四的转账,必须先获取到张三账户和李四账户两把锁,以防止转账期间其它线程对账户进行操作。如果发生这种情况:一个线程持有了张三账户,另外一个线程持有了李四账户,这时两个线程去请求另一个账户锁的时候就会请求不到。两个线程陷入无限等待状态,同时其它想要获取这两个账户锁的线程也会一直等待。
使获取锁的顺序一致解决银行转账问题
我们可以使获取锁的顺序一致来解决上面银行转账问题。上面问题导致的原因之一是,不同的线程获取锁的顺序不同。张三向李四转账,先获取张三账户锁,再获取李四账户锁;李四向张三转账,先获取李四账户锁,再获取张三账户锁,这样就会形成一个环路:线程1(张三)-》线程2(李四)-》线程1(张三)。所以我们只要破坏这个环路,无论是张三向李四转账还是李四向张三转账,它们获取锁的顺序都是一致的,就可以解决死锁问题。下面是具体实现。
/**
* 改变获取锁的顺序解决银行转账死锁问题。
*/
public class ChangeOrderDealTransferMoney implements Runnable {
TransferMoney.Acount a;
TransferMoney.Acount b;
int amount;
Object lock = new Object();
public ChangeOrderDealTransferMoney(TransferMoney.Acount a, TransferMoney.Acount b, int amount) {
this.a = a;
this.b = b;
this.amount = amount;
}
public static void main(String[] args) {
TransferMoney.Acount acount = new TransferMoney.Acount(500);
TransferMoney.Acount acount1 = new TransferMoney.Acount(500);
ChangeOrderDealTransferMoney transferMoney = new ChangeOrderDealTransferMoney(acount, acount1, 200);
ChangeOrderDealTransferMoney transferMoney2 = new ChangeOrderDealTransferMoney(acount1, acount, 200);
for (int i = 0; i < 1000; i++) {
new Thread(transferMoney).start();
new Thread(transferMoney2).start();
}
}
@Override
public void run() {
// 根据hash值来判断获取锁的顺序,如果有hash冲突,加一个竞争锁。
if (a.hashCode() > b.hashCode()) {
synchronized (a) {
synchronized (b) {
transfer(a, b, amount);
}
}
}
if (a.hashCode() < b.hashCode()) {
synchronized (b) {
synchronized (a) {
transfer(a, b, amount);
}
}
} else {
synchronized (lock) {
transfer(a, b, amount);
}
}
}
public static void transfer(TransferMoney.Acount a, TransferMoney.Acount b, int amount) {
if (a.money < amount) {
System.out.println("账户余额不足,转账失败");
}
a.money -= amount;
b.money += amount;
System.out.println("转账成功");
}
}
执行结果:
通过账户的hash值来判断获取锁的顺序,这样不论是张三向李四转账还是李四向张三转账,获取锁的顺序都一致。如果有hash冲突的情况,可以加一个竞争锁来保证线程安全。在实际生产中可以根据索引来判断获取锁的顺序。
哲学家就餐问题以及对应的解决方法
有五位哲学家在一起就餐,每位哲学家左右两边只有一个刀叉。只有拿起刀叉的时候才可以就餐。假设每个哲学家先拿左手边的餐具,再拿右手边的餐具,就完餐后就思考,思考完后又就餐,会发生什么情况。下面用代码演示一下:
/**
* 哲学家就餐问题演示。
*/
public class DiningPhilosopher implements Runnable {
Object knife;
Object cross;
public DiningPhilosopher(Object knife, Object cross) {
this.knife = knife;
this.cross = cross;
}
public static void main(String[] args) {
Object[] tableware = new Object[5];
for (int i = 0; i < tableware.length; i++) {
tableware[i] = new Object();
}
DiningPhilosopher[] diningPhilosophers = new DiningPhilosopher[5];
for (int i = 0; i < diningPhilosophers.length; i++) {
diningPhilosophers[i] = new DiningPhilosopher(tableware[i % 4], tableware[(i + 1) % 4]);
new Thread(diningPhilosophers[i]).start();
}
}
@Override
public void run() {
while (true) {
doSomething("thinking");
synchronized (knife) {
synchronized (cross) {
doSomething("eating");
doSomething("put down cross");
}
doSomething("put down knife");
}
}
}
private void doSomething(String str) {
System.out.println(Thread.currentThread().getName() + " " + str);
}
}
执行结果:
可以看到线程陷入了死锁。
解决哲学家就餐问题思路
解决哲学家就餐问题的方法有很多,从破坏死锁的四个必要条件去考虑。
1:从互斥条件考虑。餐具只能被一个人持有,所以这个条件是无法破坏的。
2:持有并请求。哲学家就餐是先拿左手边的餐具,再去拿右手边的餐具。可以让则学家同时去拿左手边的餐具和右手边的餐具。
3:不可剥夺。拿到餐具后,只有自己就餐后才放下。如果有一个服务员,可以提醒哲学家在还未就餐时放下餐具,也可以解决此问题。
4:环路等待。破坏环路等待有两种方式,一是改变一个哲学家拿餐具的顺序。第二:发餐牌,五个人发四张餐牌,只有拿到餐牌的哲学家才能就餐。
以上就是解决则学家就餐问题的一些思路。
其它线程活跃性问题
活锁
线程一直做一些无意义的事情
饥饿
线程一直得不到CPU的调度